From ada3e195278f257a6c01ac61e988feead10adccb Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Tue, 28 Apr 2026 11:46:06 +0200 Subject: [PATCH 1/8] fix some licensing issues on the docs Signed-off-by: DONNOT Benjamin --- docs/_static/colors.css | 130 +++++++ docs/_static/hacks.css | 326 ------------------ docs/_static/style.css | 5 + docs/available_envs.rst | 2 + docs/conf.py | 2 +- docs/data_pipeline.rst | 3 + docs/dive_into_time_series.rst | 3 + docs/final.rst | 2 + docs/grid2op.rst | 4 + docs/grid2op_dev.rst | 3 + docs/grid2op_dev/action.rst | 4 + docs/grid2op_dev/final.rst | 2 + docs/grid2op_extend.rst | 3 + docs/grid2op_extend/create_an_environment.rst | 3 + docs/grid2op_extend/createbackend.rst | 3 + docs/grid2op_extend/env_content.rst | 2 + docs/grid2op_extend/final.rst | 2 + docs/grid2op_extend/observation.rst | 3 + docs/grid_graph.rst | 2 + docs/gym.rst | 2 + docs/index.rst | 3 + docs/makeenv.rst | 4 + docs/mdp.rst | 3 + docs/model_based.rst | 3 + docs/model_free.rst | 3 + docs/modeled_elements.rst | 3 + docs/optimization.rst | 3 + docs/plot.rst | 4 + docs/quickstart.rst | 3 + docs/special.rst | 44 +-- docs/topology.rst | 3 +- docs/troubleshoot.rst | 2 + docs/user.rst | 3 + docs/user/action.rst | 3 + docs/user/agent.rst | 4 + docs/user/backend.rst | 3 + docs/user/chronics.rst | 3 + docs/user/converter.rst | 3 + docs/user/environment.rst | 3 + docs/user/episode.rst | 3 + docs/user/exception.rst | 3 + docs/user/final.rst | 2 + docs/user/observation.rst | 3 + docs/user/opponent.rst | 3 + docs/user/parameters.rst | 3 + docs/user/reward.rst | 3 + docs/user/rules.rst | 3 + docs/user/runner.rst | 3 + docs/user/simulator.rst | 4 + docs/user/space.rst | 3 + docs/user/special.rst | 3 +- docs/user/timeserie_handlers.rst | 3 + docs/user/utils.rst | 3 + docs/user/voltagecontroler.rst | 3 + getting_started/test_renderer_14.py | 127 ------- 55 files changed, 302 insertions(+), 476 deletions(-) create mode 100644 docs/_static/colors.css delete mode 100644 docs/_static/hacks.css delete mode 100644 getting_started/test_renderer_14.py diff --git a/docs/_static/colors.css b/docs/_static/colors.css new file mode 100644 index 000000000..0ce050339 --- /dev/null +++ b/docs/_static/colors.css @@ -0,0 +1,130 @@ +/* + SPDX-License-Identifier: MPL-2.0 + This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. +*/ + +/*Allow to add some custom colors and text decoration. + For example, :black:`text in black` or :blink:`text blinking` in rST. */ + +.aqua { + color: aqua; +} + +.black { + color: black; +} + +.blue { + color: blue; +} + +.cyan { + color: cyan; +} + +.fuchsia { + color: fuchsia; +} + +.gray { + color: gray; +} + +.green { + color: green; +} + +.lime { + color: lime; +} + +.magenta { + color: magenta; +} + +.maroon { + color: maroon; +} + +.navy { + color: navy; +} + +.olive { + color: olive; +} + +.orange { + color: orange; +} + +.pink { + color: pink; +} + +.purple { + color: purple; +} + +.red { + color: red; +} + +.silver { + color: silver; +} + +.teal { + color: teal; +} + +.white { + color: white; +} + +.yellow { + color: yellow; +} + + +/** font type **/ + +.blink { + text-decoration: blink; +} + +.it { + font-style: italic; +} + +.large { + font-size: large; +} + +.line { + text-decoration: line-through; +} + +.ob { + font-style: oblique; +} + +.over { + text-decoration: overline; +} + +.small { + font-size: small; +} + +.smallpar { + font-size: small; +} + +.strike { + text-decoration: line-through; +} + +.under { + text-decoration: underline; +} \ No newline at end of file diff --git a/docs/_static/hacks.css b/docs/_static/hacks.css deleted file mode 100644 index a0fa73de4..000000000 --- a/docs/_static/hacks.css +++ /dev/null @@ -1,326 +0,0 @@ -/* - * CSS hacks and small modification for my Sphinx website - * :copyright: Copyright 2013-2016 Lilian Besson - * :license: GPLv3, see LICENSE for details. - */ - - -/* Colors and text decoration. - For example, :black:`text in black` or :blink:`text blinking` in rST. */ - - .black { - color: black; -} - -.gray { - color: gray; -} - -.grey { - color: gray; -} - -.silver { - color: silver; -} - -.white { - color: white; -} - -.maroon { - color: maroon; -} - -.red { - color: red; -} - -.magenta { - color: magenta; -} - -.fuchsia { - color: fuchsia; -} - -.pink { - color: pink; -} - -.orange { - color: orange; -} - -.yellow { - color: yellow; -} - -.lime { - color: lime; -} - -.green { - color: green; -} - -.olive { - color: olive; -} - -.teal { - color: teal; -} - -.cyan { - color: cyan; -} - -.aqua { - color: aqua; -} - -.blue { - color: blue; -} - -.navy { - color: navy; -} - -.purple { - color: purple; -} - -.under { - text-decoration: underline; -} - -.over { - text-decoration: overline; -} - -.blink { - text-decoration: blink; -} - -.line { - text-decoration: line-through; -} - -.strike { - text-decoration: line-through; -} - -.it { - font-style: italic; -} - -.ob { - font-style: oblique; -} - -.small { - font-size: small; -} - -.large { - font-size: large; -} - -.smallpar { - font-size: small; -} - - -/* Style pour les badges en bas de la page. */ - -div.supportBadges { - margin: 1em; - text-align: right; -} - -div.supportBadges ul { - padding: 0; - display: inline; -} - -div.supportBadges li { - display: inline; -} - -div.supportBadges a { - margin-right: 1px; - opacity: 0.6; -} - -div.supportBadges a:hover { - opacity: 1; -} - - -/* Details elements in the sidebar */ - -a.reference { - border-bottom: none; - text-decoration: none; -} - -ul.details { - font-size: 80%; -} - -ul.details li p { - font-size: 85%; -} - -ul.externallinks { - font-size: 85%; -} - - -/* Pour le drapeau de langue */ - -img.languageswitch { - width: 50px; - height: 32px; - margin-left: 5px; - vertical-align: bottom; -} - -div.sphinxsidebar { - overflow: hidden !important; - font-size: 120%; - word-wrap: break-word; - width: 300px; - max-width: 300px; -} - -div.sphinxsidebar h3 { - font-size: 125%; -} - -div.sphinxsidebar h4 { - font-size: 110%; -} - -div.sphinxsidebar a { - font-size: 85%; -} - - -/* Image style for scrollUp jQuery plugin */ - -#scrollUpLeft { - bottom: 50px; - left: 260px; - height: 38px; - width: 38px; - background: url('//perso.crans.org/besson/_images/.top.svg'); - background: url('../_images/.top.svg'); -} - -@media screen and (max-width: 875px) { - #scrollUpLeft { - right: 50px; - left: auto; - } -} - - -/* responsive for font-size. */ - -@media (max-width: 875px) { - body { - font-size: 105%; - /* Increase font size for responsive theme */ - } -} - -@media (max-width: 1480px) and (min-width: 876px) { - body { - font-size: 110%; - /* Increase font size for not-so-big screens */ - } -} - -@media (min-width: 1481px) { - body { - font-size: 115%; - /* Increase even more font size for big screens */ - } -} - - -/* Social Icons in the sidebar (available: twitter, facebook, linkedin, google+, bitbucket, github) */ - -.social-icons { - display: inline-block; - margin: 0; - text-align: center; -} - -.social-icons a { - background: none no-repeat scroll center top #444444; - border: 1px solid #F6F6F6; - border-radius: 50% 50% 50% 50%; - display: inline-block; - height: 35px; - width: 35px; - margin: 0; - text-indent: -9000px; - transition: all 0.2s ease 0s; - text-align: center; - border-bottom: none; -} - -.social-icons li { - display: inline-block; - list-style-type: none; - border-bottom: none; -} -.social-icons li a { - border-bottom: none; -} - -.social-icons a:hover { - background-color: #666666; - transition: all 0.2s ease 0s; - text-decoration: none; -} - -.social-icons a.facebook { - background-image: url('../_images/.facebook.png'); - background-image: url('//perso.crans.org/besson/_images/.facebook.png'); - display: block; - margin-left: auto; - margin-right: auto; - background-size: 35px 35px; -} - -.social-icons a.bitbucket { - background-image: url('../_images/.bitbucket.png'); - background-image: url('//perso.crans.org/besson/_images/.bitbucket.png'); - display: block; - margin-left: auto; - margin-right: auto; - background-size: 35px 35px; -} - -.social-icons li a.github { - background-image: url('../_images/.github.png'); - background-image: url('//perso.crans.org/besson/_images/.github.png'); - display: block; - margin-left: auto; - margin-right: auto; - background-size: 35px 35px; -} - -.social-icons li a.wikipedia { - background-image: url('../_images/.wikipedia.png'); - background-image: url('//perso.crans.org/besson/_images/.wikipedia.png'); - display: block; - margin-left: auto; - margin-right: auto; - background-size: 35px 35px; -} \ No newline at end of file diff --git a/docs/_static/style.css b/docs/_static/style.css index 7269f2cde..7f0813316 100644 --- a/docs/_static/style.css +++ b/docs/_static/style.css @@ -1,3 +1,8 @@ +/* + SPDX-License-Identifier: MPL-2.0 + This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. +*/ + /* this stuff uses a couple of themes as a base with some custom stuff added In particular thanks to: diff --git a/docs/available_envs.rst b/docs/available_envs.rst index f2c8da22c..8e30f9820 100644 --- a/docs/available_envs.rst +++ b/docs/available_envs.rst @@ -1,3 +1,5 @@ +.. SPDX-License-Identifier: MPL-2.0 +.. This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. .. |l2rpn_case14_sandbox_layout| image:: ./img/l2rpn_case14_sandbox_layout.png .. |R2_full_grid| image:: ./img/R2_full_grid.png diff --git a/docs/conf.py b/docs/conf.py index a83a3073f..0b0dfb321 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -76,7 +76,7 @@ # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] -html_css_files = ['hacks.css'] +html_css_files = ['colors.css'] # for pdf pdf_documents = [('index', u'rst2pdf', u'Grid2op documentation', u'B. DONNOT'),] diff --git a/docs/data_pipeline.rst b/docs/data_pipeline.rst index 0c316667e..f78fc7cee 100644 --- a/docs/data_pipeline.rst +++ b/docs/data_pipeline.rst @@ -1,3 +1,6 @@ +.. SPDX-License-Identifier: MPL-2.0 +.. This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. + .. _environment-module-data-pipeline: Optimize the data pipeline diff --git a/docs/dive_into_time_series.rst b/docs/dive_into_time_series.rst index 5a5264996..04965c3be 100644 --- a/docs/dive_into_time_series.rst +++ b/docs/dive_into_time_series.rst @@ -1,3 +1,6 @@ +.. SPDX-License-Identifier: MPL-2.0 +.. This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. + .. _chornix2grid_github: https://github.com/bdonnot/chronix2grid .. _doc_timeseries: diff --git a/docs/final.rst b/docs/final.rst index f4aef68aa..46b39ad6c 100644 --- a/docs/final.rst +++ b/docs/final.rst @@ -1,3 +1,5 @@ +.. SPDX-License-Identifier: MPL-2.0 +.. This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. If you still can't find what you're looking for, try in one of the following pages: diff --git a/docs/grid2op.rst b/docs/grid2op.rst index 93f1f0e43..658a55144 100644 --- a/docs/grid2op.rst +++ b/docs/grid2op.rst @@ -1,4 +1,8 @@ +.. SPDX-License-Identifier: MPL-2.0 +.. This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. + .. module:: grid2op + .. _grid2op-module: Grid2Op module diff --git a/docs/grid2op_dev.rst b/docs/grid2op_dev.rst index 3d44046de..c777bf17d 100644 --- a/docs/grid2op_dev.rst +++ b/docs/grid2op_dev.rst @@ -1,3 +1,6 @@ +.. SPDX-License-Identifier: MPL-2.0 +.. This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. + .. toctree:: :maxdepth: 1 diff --git a/docs/grid2op_dev/action.rst b/docs/grid2op_dev/action.rst index 3cf669624..d81f0a89d 100644 --- a/docs/grid2op_dev/action.rst +++ b/docs/grid2op_dev/action.rst @@ -1,3 +1,7 @@ +.. SPDX-License-Identifier: MPL-2.0 +.. This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. + + How to add a new type of action =================================== diff --git a/docs/grid2op_dev/final.rst b/docs/grid2op_dev/final.rst index f095ba7ca..4178cbef9 100644 --- a/docs/grid2op_dev/final.rst +++ b/docs/grid2op_dev/final.rst @@ -1,2 +1,4 @@ +.. SPDX-License-Identifier: MPL-2.0 +.. This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. .. include:: ../final.rst diff --git a/docs/grid2op_extend.rst b/docs/grid2op_extend.rst index 1bce8296b..9cda0bb3d 100644 --- a/docs/grid2op_extend.rst +++ b/docs/grid2op_extend.rst @@ -1,3 +1,6 @@ +.. SPDX-License-Identifier: MPL-2.0 +.. This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. + .. toctree:: :maxdepth: 1 diff --git a/docs/grid2op_extend/create_an_environment.rst b/docs/grid2op_extend/create_an_environment.rst index f7f97d7c2..0b788a131 100644 --- a/docs/grid2op_extend/create_an_environment.rst +++ b/docs/grid2op_extend/create_an_environment.rst @@ -1,3 +1,6 @@ +.. SPDX-License-Identifier: MPL-2.0 +.. This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. + .. |l2rpn_case14_sandbox_layout| image:: ../img/l2rpn_case14_sandbox_layout.png .. |R2_full_grid| image:: ../img/R2_full_grid.png .. |l2rpn_neurips_2020_track1_layout| image:: ../img/l2rpn_neurips_2020_track1_layout.png diff --git a/docs/grid2op_extend/createbackend.rst b/docs/grid2op_extend/createbackend.rst index 124925f88..49ab08c61 100644 --- a/docs/grid2op_extend/createbackend.rst +++ b/docs/grid2op_extend/createbackend.rst @@ -1,3 +1,6 @@ +.. SPDX-License-Identifier: MPL-2.0 +.. This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. + .. _name_load: ./space.html#grid2op.Space.GridObjects.name_load .. _name_gen: ./space.html#grid2op.Space.GridObjects.name_gen .. _name_line: ./space.html#grid2op.Space.GridObjects.name_line diff --git a/docs/grid2op_extend/env_content.rst b/docs/grid2op_extend/env_content.rst index c4351185a..3d08c6817 100644 --- a/docs/grid2op_extend/env_content.rst +++ b/docs/grid2op_extend/env_content.rst @@ -1,3 +1,5 @@ +.. SPDX-License-Identifier: MPL-2.0 +.. This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. .. |l2rpn_case14_sandbox_layout| image:: ../img/l2rpn_case14_sandbox_layout.png .. |R2_full_grid| image:: ../img/R2_full_grid.png diff --git a/docs/grid2op_extend/final.rst b/docs/grid2op_extend/final.rst index f095ba7ca..4178cbef9 100644 --- a/docs/grid2op_extend/final.rst +++ b/docs/grid2op_extend/final.rst @@ -1,2 +1,4 @@ +.. SPDX-License-Identifier: MPL-2.0 +.. This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. .. include:: ../final.rst diff --git a/docs/grid2op_extend/observation.rst b/docs/grid2op_extend/observation.rst index 29e738660..2b4e98f42 100644 --- a/docs/grid2op_extend/observation.rst +++ b/docs/grid2op_extend/observation.rst @@ -1,3 +1,6 @@ +.. SPDX-License-Identifier: MPL-2.0 +.. This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. + How to add a new attribute to the observation ============================================== diff --git a/docs/grid_graph.rst b/docs/grid_graph.rst index 3d78338a5..13c54fc6c 100644 --- a/docs/grid_graph.rst +++ b/docs/grid_graph.rst @@ -1,3 +1,5 @@ +.. SPDX-License-Identifier: MPL-2.0 +.. This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. .. |grid_graph_1| image:: ./img/grid_graph_1.png :width: 45% diff --git a/docs/gym.rst b/docs/gym.rst index 931bb0935..74a471741 100644 --- a/docs/gym.rst +++ b/docs/gym.rst @@ -1,3 +1,5 @@ +.. SPDX-License-Identifier: MPL-2.0 +.. This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. .. _BaseGymSpaceConverter.add_key: ./gym.html#grid2op.gym_compat.gym_space_converter._BaseGymSpaceConverter.add_key .. _BaseGymSpaceConverter.keep_only_attr: ./gym.html#grid2op.gym_compat.gym_space_converter._BaseGymSpaceConverter.keep_only_attr diff --git a/docs/index.rst b/docs/index.rst index 6cf59ea3b..340a48408 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,3 +1,6 @@ +.. SPDX-License-Identifier: MPL-2.0 +.. This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. + .. Grid2Op documentation master file, created by sphinx-quickstart on Wed Jul 24 15:07:20 2019. You can adapt this file completely to your liking, but it should at least diff --git a/docs/makeenv.rst b/docs/makeenv.rst index f30933401..fd5fedab9 100644 --- a/docs/makeenv.rst +++ b/docs/makeenv.rst @@ -1,4 +1,8 @@ +.. SPDX-License-Identifier: MPL-2.0 +.. This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. + .. currentmodule:: grid2op.MakeEnv + .. _make-env-module: Make: Using pre defined Environments diff --git a/docs/mdp.rst b/docs/mdp.rst index c889287ed..f1d2715ae 100644 --- a/docs/mdp.rst +++ b/docs/mdp.rst @@ -1,3 +1,6 @@ +.. SPDX-License-Identifier: MPL-2.0 +.. This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. + .. for the color .. include:: special.rst diff --git a/docs/model_based.rst b/docs/model_based.rst index c0d3eda9b..4b2d332f6 100644 --- a/docs/model_based.rst +++ b/docs/model_based.rst @@ -1,3 +1,6 @@ +.. SPDX-License-Identifier: MPL-2.0 +.. This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. + .. _model_based_rl: Model Based / Planning methods diff --git a/docs/model_free.rst b/docs/model_free.rst index 10424d7c7..d1f6f6a3b 100644 --- a/docs/model_free.rst +++ b/docs/model_free.rst @@ -1,3 +1,6 @@ +.. SPDX-License-Identifier: MPL-2.0 +.. This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. + .. _model_free_rl: Model Free Reinforcement Learning diff --git a/docs/modeled_elements.rst b/docs/modeled_elements.rst index 9dc4509d3..0df3351de 100644 --- a/docs/modeled_elements.rst +++ b/docs/modeled_elements.rst @@ -1,3 +1,6 @@ +.. SPDX-License-Identifier: MPL-2.0 +.. This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. + .. |5subs_grid_layout| image:: ./img/5subs_grid_layout.jpg .. |5subs_grid_5_sub1_graph| image:: ./img/5subs_grid_5_sub1_graph.jpg .. |5subs_grid_all_1| image:: ./img/5subs_grid_all_1.jpg diff --git a/docs/optimization.rst b/docs/optimization.rst index ba9407a8e..2a06a5908 100644 --- a/docs/optimization.rst +++ b/docs/optimization.rst @@ -1,3 +1,6 @@ +.. SPDX-License-Identifier: MPL-2.0 +.. This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. + .. _optimization_page: Using "optimization" technique diff --git a/docs/plot.rst b/docs/plot.rst index ab7f6f93e..3cee29632 100644 --- a/docs/plot.rst +++ b/docs/plot.rst @@ -1,4 +1,8 @@ +.. SPDX-License-Identifier: MPL-2.0 +.. This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. + .. currentmodule:: grid2op.PlotGrid + .. |replaygif| image:: ./img/random_agent.gif .. |14bus_1| image:: ./img/14bus_1.png .. |14bus_2| image:: ./img/14bus_2.png diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 3694626c5..d60018bd9 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -1,3 +1,6 @@ +.. SPDX-License-Identifier: MPL-2.0 +.. This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. + Getting started =================================== diff --git a/docs/special.rst b/docs/special.rst index 68ffebc55..d63b1c786 100644 --- a/docs/special.rst +++ b/docs/special.rst @@ -1,28 +1,33 @@ +.. SPDX-License-Identifier: MPL-2.0 +.. This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. + .. Color profiles for Sphinx. -.. Has to be used with hacks.css -.. (https://bitbucket.org/lbesson/web-sphinx/src/master/.static/hacks.css) +.. You can use it only if you have said, in config.py that the "colors.css" file should be included +.. with something like : +.. +.. html_static_path = ['_static'] +.. html_css_files = ['colors.css'] +.. role:: aqua .. role:: black -.. role:: gray -.. role:: grey -.. role:: silver -.. role:: white -.. role:: maroon -.. role:: red -.. role:: magenta +.. role:: blue +.. role:: cyan .. role:: fuchsia -.. role:: pink -.. role:: orange -.. role:: yellow -.. role:: lime +.. role:: gray .. role:: green -.. role:: olive -.. role:: teal -.. role:: cyan -.. role:: aqua -.. role:: blue +.. role:: lime +.. role:: magenta +.. role:: maroon .. role:: navy +.. role:: olive +.. role:: orange +.. role:: pink .. role:: purple +.. role:: red +.. role:: silver +.. role:: teal +.. role:: white +.. role:: yellow .. role:: under .. role:: over @@ -33,10 +38,9 @@ .. role:: it .. role:: ob -.. role:: small .. role:: large +.. role:: small .. role:: center .. role:: left .. role:: right -.. (c) Lilian Besson, 2011-2016, https://bitbucket.org/lbesson/web-sphinx/ diff --git a/docs/topology.rst b/docs/topology.rst index 2c81c7226..924bb7fbc 100644 --- a/docs/topology.rst +++ b/docs/topology.rst @@ -1,4 +1,5 @@ - +.. SPDX-License-Identifier: MPL-2.0 +.. This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. .. _topology-modeling-module: diff --git a/docs/troubleshoot.rst b/docs/troubleshoot.rst index d07539fda..2f13f1cb2 100644 --- a/docs/troubleshoot.rst +++ b/docs/troubleshoot.rst @@ -1,3 +1,5 @@ +.. SPDX-License-Identifier: MPL-2.0 +.. This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. .. _troubleshoot_page: diff --git a/docs/user.rst b/docs/user.rst index d1b715423..16f832bf2 100644 --- a/docs/user.rst +++ b/docs/user.rst @@ -1,3 +1,6 @@ +.. SPDX-License-Identifier: MPL-2.0 +.. This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. + .. toctree:: :maxdepth: 1 diff --git a/docs/user/action.rst b/docs/user/action.rst index 1f9f0bb28..52bad55c7 100644 --- a/docs/user/action.rst +++ b/docs/user/action.rst @@ -1,3 +1,6 @@ +.. SPDX-License-Identifier: MPL-2.0 +.. This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. + .. currentmodule:: grid2op.Action .. _n_gen: ./space.html#grid2op.Space.GridObjects.n_gen diff --git a/docs/user/agent.rst b/docs/user/agent.rst index 557603878..4ab111f5f 100644 --- a/docs/user/agent.rst +++ b/docs/user/agent.rst @@ -1,4 +1,8 @@ +.. SPDX-License-Identifier: MPL-2.0 +.. This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. + .. currentmodule:: grid2op.Agent + .. _agent-module: Agent diff --git a/docs/user/backend.rst b/docs/user/backend.rst index 1d52adccd..9730d6057 100644 --- a/docs/user/backend.rst +++ b/docs/user/backend.rst @@ -1,3 +1,6 @@ +.. SPDX-License-Identifier: MPL-2.0 +.. This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. + .. currentmodule:: grid2op.Backend .. _backend-module: diff --git a/docs/user/chronics.rst b/docs/user/chronics.rst index 1557ab07f..91338a9fe 100644 --- a/docs/user/chronics.rst +++ b/docs/user/chronics.rst @@ -1,3 +1,6 @@ +.. SPDX-License-Identifier: MPL-2.0 +.. This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. + .. currentmodule:: grid2op.Chronics .. _time-series-module: diff --git a/docs/user/converter.rst b/docs/user/converter.rst index 3f8e5c8c8..45dd9cb61 100644 --- a/docs/user/converter.rst +++ b/docs/user/converter.rst @@ -1,3 +1,6 @@ +.. SPDX-License-Identifier: MPL-2.0 +.. This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. + .. currentmodule:: grid2op.Converter Converters diff --git a/docs/user/environment.rst b/docs/user/environment.rst index 5c1c9613a..55f14f24f 100644 --- a/docs/user/environment.rst +++ b/docs/user/environment.rst @@ -1,3 +1,6 @@ +.. SPDX-License-Identifier: MPL-2.0 +.. This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. + .. currentmodule:: grid2op.Environment .. _environment-module: diff --git a/docs/user/episode.rst b/docs/user/episode.rst index 9d8be3d8f..72bd59dcb 100644 --- a/docs/user/episode.rst +++ b/docs/user/episode.rst @@ -1,3 +1,6 @@ +.. SPDX-License-Identifier: MPL-2.0 +.. This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. + Episode =================================== diff --git a/docs/user/exception.rst b/docs/user/exception.rst index ac842250f..694a48f68 100644 --- a/docs/user/exception.rst +++ b/docs/user/exception.rst @@ -1,3 +1,6 @@ +.. SPDX-License-Identifier: MPL-2.0 +.. This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. + Exception =================================== This page is organized as follow: diff --git a/docs/user/final.rst b/docs/user/final.rst index 79beb6191..f768718e1 100644 --- a/docs/user/final.rst +++ b/docs/user/final.rst @@ -1,2 +1,4 @@ +.. SPDX-License-Identifier: MPL-2.0 +.. This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. .. include:: ../final.rst \ No newline at end of file diff --git a/docs/user/observation.rst b/docs/user/observation.rst index 05bb35a75..410e3caf7 100644 --- a/docs/user/observation.rst +++ b/docs/user/observation.rst @@ -1,3 +1,6 @@ +.. SPDX-License-Identifier: MPL-2.0 +.. This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. + .. currentmodule:: grid2op.Observation .. include:: special.rst diff --git a/docs/user/opponent.rst b/docs/user/opponent.rst index 0998a3d36..efc94ea56 100644 --- a/docs/user/opponent.rst +++ b/docs/user/opponent.rst @@ -1,3 +1,6 @@ +.. SPDX-License-Identifier: MPL-2.0 +.. This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. + .. currentmodule:: grid2op.Opponent Opponent Modeling diff --git a/docs/user/parameters.rst b/docs/user/parameters.rst index 727a422e5..8e219d551 100644 --- a/docs/user/parameters.rst +++ b/docs/user/parameters.rst @@ -1,3 +1,6 @@ +.. SPDX-License-Identifier: MPL-2.0 +.. This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. + .. _parameters-module: Parameters diff --git a/docs/user/reward.rst b/docs/user/reward.rst index 684eaccaf..c439da55a 100644 --- a/docs/user/reward.rst +++ b/docs/user/reward.rst @@ -1,3 +1,6 @@ +.. SPDX-License-Identifier: MPL-2.0 +.. This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. + .. currentmodule:: grid2op.Reward .. _reward-module: diff --git a/docs/user/rules.rst b/docs/user/rules.rst index a6967fccf..c2bb9f258 100644 --- a/docs/user/rules.rst +++ b/docs/user/rules.rst @@ -1,3 +1,6 @@ +.. SPDX-License-Identifier: MPL-2.0 +.. This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. + .. currentmodule:: grid2op.Rules .. _rule-module: diff --git a/docs/user/runner.rst b/docs/user/runner.rst index 8f96ffaf8..d8b7337b8 100644 --- a/docs/user/runner.rst +++ b/docs/user/runner.rst @@ -1,3 +1,6 @@ +.. SPDX-License-Identifier: MPL-2.0 +.. This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. + .. _runner-module: Runner diff --git a/docs/user/simulator.rst b/docs/user/simulator.rst index 0ce752046..902a1580a 100644 --- a/docs/user/simulator.rst +++ b/docs/user/simulator.rst @@ -1,4 +1,8 @@ +.. SPDX-License-Identifier: MPL-2.0 +.. This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. + .. currentmodule:: grid2op.simulator + .. _simulator_page: Simulator diff --git a/docs/user/space.rst b/docs/user/space.rst index ac4f4b747..e810672ff 100644 --- a/docs/user/space.rst +++ b/docs/user/space.rst @@ -1,3 +1,6 @@ +.. SPDX-License-Identifier: MPL-2.0 +.. This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. + .. currentmodule:: grid2op.Space Space diff --git a/docs/user/special.rst b/docs/user/special.rst index 8f6680780..f0939d05e 100644 --- a/docs/user/special.rst +++ b/docs/user/special.rst @@ -1,4 +1,5 @@ -.. for the color +.. SPDX-License-Identifier: MPL-2.0 +.. This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. .. include:: ../special.rst diff --git a/docs/user/timeserie_handlers.rst b/docs/user/timeserie_handlers.rst index bd76abddf..4d6e849cc 100644 --- a/docs/user/timeserie_handlers.rst +++ b/docs/user/timeserie_handlers.rst @@ -1,3 +1,6 @@ +.. SPDX-License-Identifier: MPL-2.0 +.. This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. + .. currentmodule:: grid2op.Chronics.handlers .. _tshandler-module: diff --git a/docs/user/utils.rst b/docs/user/utils.rst index d30de21a1..40c6a5b64 100644 --- a/docs/user/utils.rst +++ b/docs/user/utils.rst @@ -1,3 +1,6 @@ +.. SPDX-License-Identifier: MPL-2.0 +.. This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. + .. currentmodule:: grid2op.utils Utility classes diff --git a/docs/user/voltagecontroler.rst b/docs/user/voltagecontroler.rst index 1c85a3552..3bec11940 100644 --- a/docs/user/voltagecontroler.rst +++ b/docs/user/voltagecontroler.rst @@ -1,3 +1,6 @@ +.. SPDX-License-Identifier: MPL-2.0 +.. This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. + .. currentmodule:: grid2op.VoltageControler .. _voltage-controler-module: diff --git a/getting_started/test_renderer_14.py b/getting_started/test_renderer_14.py deleted file mode 100644 index c67db2fa0..000000000 --- a/getting_started/test_renderer_14.py +++ /dev/null @@ -1,127 +0,0 @@ -import grid2op -from grid2op.Agent import DoNothingAgent -from grid2op.Agent import GreedyAgent, RandomAgent -import numpy as np -import pdb -import warnings - - -with warnings.catch_warnings(): - warnings.filterwarnings("ignore") - env = grid2op.make("case14_realistic") - - -class MyExpertAgent(GreedyAgent): - def __init__(self, action_space): - GreedyAgent.__init__(self, action_space) - self.saved_score = [] - - def act(self, observation, reward, done=False): - """ - By definition, all "greedy" agents are acting the same way. The only thing that can differentiate multiple - agents is the actions that are tested. - - These actions are defined in the method :func:`._get_tested_action`. This :func:`.act` method implements the - greedy logic: take the actions that maximizes the instantaneous reward on the simulated action. - - Parameters - ---------- - observation: :class:`grid2op.BaseObservation.BaseObservation` - The current observation of the :class:`grid2op.Environment` - - reward: ``float`` - The current reward. This is the reward obtained by the previous action - - done: ``bool`` - Whether the episode has ended or not. Used to maintain gym compatibility - - Returns - ------- - res: :class:`grid2op.BaseAction.BaseAction` - The action chosen by the bot / controller / agent. - - """ - # print("________________\nbeginning simulate") - self.tested_action = self._get_tested_action(observation) - if len(self.tested_action) > 1: - all_rewards = np.full(shape=len(self.tested_action), fill_value=np.NaN, dtype=np.float) - for i, action in enumerate(self.tested_action): - simul_obs, simul_reward, simul_has_error, simul_info = observation.simulate(action) - all_rewards[i] = simul_reward - # if simul_reward > 19: - # pdb.set_trace() - - reward_idx = np.argmax(all_rewards) # rewards.index(max(rewards)) - expected_reward = np.max(all_rewards) - best_action = self.tested_action[reward_idx] -# print("BaseAction taken:\n{}".format(best_action)) - else: - all_rewards = [None] - expected_reward = None - best_action = self.tested_action[0] - - self.saved_score.append(((best_action, expected_reward), - [el for el in zip(self.tested_action, all_rewards)])) - # print("end simulate\n_____________") - return best_action - - def _get_tested_action(self, observation): - res = [self.action_space({})] # add the do nothing - for i, el in enumerate(observation.line_status): - # try to reconnect powerlines - if not el: - tmp = np.zeros(self.action_space.n_line, dtype=np.int) - tmp[i] = 1 - action = self.action_space({"set_line_status": tmp}) - action = action.update({"set_bus": {"lines_or_id": [(i, 1)], "lines_ex_id": [(i, 1)]}}) - res.append(action) - - # disconnect the powerlines - ## 12 to 13, 10 to 9 # 5 to 12, 5 to 10, - for i in [19, 17]: # , 10 ,12 <- with that it takes action that leads to divergence, check that! - tmp = np.full(self.action_space.n_line, fill_value=False, dtype=np.bool) - tmp[i] = True - action = self.action_space({"change_line_status": tmp}) - if not observation.line_status[i]: - # so the action consisted in reconnecting the powerline - # i need to say on which bus - action = action.update({"set_bus": {"lines_or_id": [(i, 1)], "lines_ex_id": [(i, 1)]}}) - res.append(action) - - # play with the topology - ## i put powerlines going from 1 to 4 with powerline going from 3 to 4 at substation 4 - action = self.action_space({"change_bus": - {"substations_id": [(4, np.array([False, True, True, False, False]))]}}) - res.append(action) - - ## i put powerline from 5 to 12 with powerline from 5 to 10 at substation 5 - action = self.action_space({"change_bus": - {"substations_id": [(5, np.array([False, True, False, True, False, False]))]}}) - res.append(action) - - ## i put powerline from 1 to 4 with powerline from 1 to 3 with at substation 1 - action = self.action_space({"change_bus": - {"substations_id": [(1, np.array([False, False, True, True, False, False]))]}}) - res.append(action) - return res - - -my_agent = MyExpertAgent(env.action_space) -# my_agent = RandomAgent(env.action_space) -print("Total unitary action possible: {}".format(my_agent.action_space.n)) - -all_obs = [] -obs = env.reset() -all_obs.append(obs) -reward = env.reward_range[0] -done = False -nb_step = 0 -while True: - env.render() - action = my_agent.act(obs, reward, done) - obs, reward, done, _ = env.step(action) - print("Rendering timestep {}".format(nb_step)) - if done: - break - all_obs.append(obs) - nb_step += 1 From 8435be7dd86c1688d21f79fdaea4a67ac351e051 Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Tue, 28 Apr 2026 11:46:59 +0200 Subject: [PATCH 2/8] refactor the tests on circle ci to reduce execution time Signed-off-by: DONNOT Benjamin --- .circleci/config.yml | 55 ++++++++++++++++++++++++-- grid2op/tests/helper_list_test.py | 25 ++++++++++++ grid2op/tests/test_BackendAction.py | 61 ++++++++++++++++------------- 3 files changed, 110 insertions(+), 31 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index a1474b2e9..bcf68aa34 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -93,7 +93,7 @@ jobs: source venv_test/bin/activate export _GRID2OP_FORCE_TEST=1 cd grid2op/tests/ - python -m unittest -v test_Agent test_AgentsFast test_recopowerlineperarea + python -m unittest -v test_Agent test_AgentsFast test_recopowerlineperarea test_Opponent test_baseline_alert test_converter: executor: grid2op-executor @@ -168,8 +168,8 @@ jobs: source venv_test/bin/activate export _GRID2OP_FORCE_TEST=1 cd grid2op/tests/ - python -m unittest -v test_attached_envs test_attached_envs_compat test_l2rpn_idf_2023 test_MultiMix test_timeOutEnvironment test_MaskedEnvironment test_MakeEnv test_multi_steps_env test_simenv_blackout test_get_default_env_kwargs - + python -m unittest -v test_attached_envs test_attached_envs_compat test_l2rpn_idf_2023 test_MultiMix test_timeOutEnvironment test_MaskedEnvironment test_MakeEnv test_multi_steps_env test_simenv_blackout test_get_default_env_kwargs test_Environment test_EnvironmentCpy test_generate_classes test_basic_env_ls + test_alert_alarm: executor: grid2op-executor resource_class: small @@ -195,6 +195,54 @@ jobs: cd grid2op/tests/ python -m unittest -v test_AlarmFeature test_alert_gym_compat test_alert_obs_act test_alert_trust_score test_AlertReward + test_time_series: + executor: grid2op-executor + resource_class: small + steps: + - checkout + - run: apt-get update -y + - run: python -m pip install virtualenv + - run: python -m virtualenv venv_test + - run: + command: | + source venv_test/bin/activate + pip install -U pip setuptools wheel + - run: + command: | + source venv_test/bin/activate + pip install -e .[test] + pip freeze + - run: + command: | + source venv_test/bin/activate + export _GRID2OP_FORCE_TEST=1 + cd grid2op/tests/ + python -m unittest -v test_ts_handlers test_ChronicsHandler test_env_from_episode + + test_backend: + executor: grid2op-executor + resource_class: small + steps: + - checkout + - run: apt-get update -y + - run: python -m pip install virtualenv + - run: python -m virtualenv venv_test + - run: + command: | + source venv_test/bin/activate + pip install -U pip setuptools wheel + - run: + command: | + source venv_test/bin/activate + pip install -e .[test] + pip freeze + - run: + command: | + source venv_test/bin/activate + export _GRID2OP_FORCE_TEST=1 + cd grid2op/tests/ + python -m unittest -v test_ts_handlers test_basicBackendInterface test_action_set_orig_state test_PandaPowerBackendDefaultFunc test_PandaPowerBackend test_BackendAction + test_issue: executor: grid2op-executor resource_class: small @@ -602,6 +650,7 @@ workflows: - test_env_general - test_alert_alarm - test_issue + - test_time_series - legacy_lightsim_old_pp - legacy_lightsim - test_chronix2grid diff --git a/grid2op/tests/helper_list_test.py b/grid2op/tests/helper_list_test.py index e85079926..77cec8ee8 100755 --- a/grid2op/tests/helper_list_test.py +++ b/grid2op/tests/helper_list_test.py @@ -3,6 +3,7 @@ li_tested_elsewhere = [ # agent (approx 1 min) "test_Agent", "test_AgentsFast", "test_recopowerlineperarea", + "test_Opponent", "test_baseline_alert", # converter (approx 45s) "test_AgentConverter", "test_Converter", "test_BackendConverter", # Runner / EpisodeData / "score (3 mins)" @@ -31,12 +32,27 @@ "test_multi_steps_env", "test_simenv_blackout", "test_get_default_env_kwargs", + "test_Environment", + "test_EnvironmentCpy", + "test_generate_classes", + "test_basic_env_ls", # alert / alarm ( 1min) "test_AlarmFeature", "test_alert_gym_compat", "test_alert_obs_act", "test_alert_trust_score", "test_AlertReward", + # time_series / backend + "test_ts_handlers", + "test_ChronicsHandler", + "test_env_from_episode", + # backend + "test_basicBackendInterface", + "test_action_set_orig_state", + "test_PandaPowerBackendDefaultFunc", + "test_PandaPowerBackend", + "test_BackendAction" + # TODO simulate # TODO curtailment ] @@ -60,6 +76,15 @@ def print_suite(suite): do_print = False break test_name = "{}.{}.{}".format(testmodule, testsuite, testmethod) + + # sometimes tests are included in other tests + # and so they are executed like "grid2op.tests.test_Agent.TestAgent.test_0_donothing" + # this prevents it + for el in li_tested_elsewhere: + if test_name.startswith("grid2op.tests") and el in test_name: + do_print = False + break + if do_print: print(test_name) diff --git a/grid2op/tests/test_BackendAction.py b/grid2op/tests/test_BackendAction.py index 2a0918d85..e7af90f47 100644 --- a/grid2op/tests/test_BackendAction.py +++ b/grid2op/tests/test_BackendAction.py @@ -53,13 +53,13 @@ def apply_action(self, backendAction=None): shunts__, ) = backendAction() - tmp_prod_p = self._get_vector_inj["prod_p"](self._grid) + # tmp_prod_p = self._get_vector_inj["prod_p"](self._grid) if np.any(prod_p.changed): - tmp_prod_p.iloc[prod_p.changed] = prod_p.values[prod_p.changed] + self._grid.gen.iloc[prod_p.changed, self._prod_p_col_id] = prod_p.values[prod_p.changed] - tmp_prod_v = self._get_vector_inj["prod_v"](self._grid) + # tmp_prod_v = self._get_vector_inj["prod_v"](self._grid) if np.any(prod_v.changed): - tmp_prod_v.iloc[prod_v.changed] = ( + self._grid.gen.iloc[prod_v.changed, self._prod_v_col_id] = ( prod_v.values[prod_v.changed] / self.prod_pu_to_kv[prod_v.changed] ) @@ -67,13 +67,13 @@ def apply_action(self, backendAction=None): # handling of the slack bus, where "2" generators are present. self._grid["ext_grid"]["vm_pu"] = 1.0 * tmp_prod_v[self._id_bus_added] - tmp_load_p = self._get_vector_inj["load_p"](self._grid) + # tmp_load_p = self._get_vector_inj["load_p"](self._grid) if np.any(load_p.changed): - tmp_load_p.iloc[load_p.changed] = load_p.values[load_p.changed] + self._grid.load.iloc[load_p.changed, self._load_p_col_id] = load_p.values[load_p.changed] - tmp_load_q = self._get_vector_inj["load_q"](self._grid) + # tmp_load_q = self._get_vector_inj["load_q"](self._grid) if np.any(load_q.changed): - tmp_load_q.iloc[load_q.changed] = load_q.values[load_q.changed] + self._grid.load.iloc[load_q.changed, self._load_q_col_id] = load_q.values[load_q.changed] if type(self).shunts_data_available: shunt_p, shunt_q, shunt_bus = shunts__ @@ -106,10 +106,10 @@ def apply_action(self, backendAction=None): loads_bus = backendAction.get_loads_bus() for load_id, new_bus in loads_bus: if new_bus == -1: - self._grid.load["in_service"][load_id] = False + self._grid.load.iloc[load_id, self._in_service_load_col_id] = False else: - self._grid.load["in_service"][load_id] = True - self._grid.load["bus"][load_id] = ( + self._grid.load.iloc[load_id, self._in_service_load_col_id] = True + self._grid.load.iloc[load_id, self._bus_load_col_id] = ( self.load_to_subid[load_id] + (new_bus - 1) * self._nb_bus_before_for_test ) @@ -117,10 +117,10 @@ def apply_action(self, backendAction=None): gens_bus = backendAction.get_gens_bus() for gen_id, new_bus in gens_bus: if new_bus == -1: - self._grid.gen["in_service"][gen_id] = False + self._grid.gen.iloc[gen_id, self._in_service_gen_col_id] = False else: - self._grid.gen["in_service"][gen_id] = True - self._grid.gen["bus"][gen_id] = ( + self._grid.gen.iloc[gen_id, self._in_service_gen_col_id] = True + self._grid.gen.iloc[gen_id, self._bus_gen_col_id] = ( self.gen_to_subid[gen_id] + (new_bus - 1) * self._nb_bus_before_for_test ) @@ -138,18 +138,20 @@ def apply_action(self, backendAction=None): for line_id, new_bus in lines_or_bus: if line_id < self._nb_line_for_test: dt = self._grid.line - key = "from_bus" line_id_db = line_id + is_col_id = self._in_service_line_col_id + bus_col_id = self._from_bus_line_col_id else: dt = self._grid.trafo - key = "hv_bus" line_id_db = line_id - self._nb_line_for_test + is_col_id = self._in_service_trafo_col_id + bus_col_id = self._hv_bus_trafo_col_id if new_bus == -1: - dt["in_service"][line_id_db] = False + dt.iloc[line_id_db, is_col_id] = False else: - dt["in_service"][line_id_db] = True - dt[key][line_id_db] = ( + dt.iloc[line_id_db, is_col_id] = True + dt.iloc[line_id_db, bus_col_id] = ( self.line_or_to_subid[line_id] + (new_bus - 1) * self._nb_bus_before_for_test ) @@ -158,26 +160,29 @@ def apply_action(self, backendAction=None): for line_id, new_bus in lines_ex_bus: if line_id < self._nb_line_for_test: dt = self._grid.line - key = "to_bus" line_id_db = line_id + is_col_id = self._in_service_line_col_id + bus_col_id = self._to_bus_line_col_id else: dt = self._grid.trafo - key = "lv_bus" line_id_db = line_id - self._nb_line_for_test + is_col_id = self._in_service_trafo_col_id + bus_col_id = self._lv_bus_trafo_col_id if new_bus == -1: - dt["in_service"][line_id_db] = False + dt.iloc[line_id_db, is_col_id] = False else: - dt["in_service"][line_id_db] = True - dt[key][line_id_db] = ( + dt.iloc[line_id_db, is_col_id] = True + dt.iloc[line_id_db, bus_col_id] = ( self.line_ex_to_subid[line_id] + (new_bus - 1) * self._nb_bus_before_for_test ) - bus_is = self._grid.bus["in_service"] + # bus_is = self._grid.bus["in_service"] + bus_id_col = (self._grid.bus.columns == "in_service").nonzero()[0][0] for i, (bus1_status, bus2_status) in enumerate(active_bus): - bus_is[i] = bus1_status # no iloc for bus, don't ask me why please :-/ - bus_is[i + self._nb_bus_before_for_test] = bus2_status + self._grid.bus.iloc[i, bus_id_col] = bus1_status # no iloc for bus, don't ask me why please :-/ + self._grid.bus.iloc[i + self._nb_bus_before_for_test, bus_id_col] = bus2_status class TestXXXBus(unittest.TestCase): @@ -199,7 +204,7 @@ def setUp(self) -> None: ) seed = 0 self.nb_test = 10 - self.max_iter = 30 + self.max_iter = 10 self.envref.seed(seed) self.envtest.seed(seed) From f42f1638868586ff6a752565e2b3df2007388772 Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Tue, 28 Apr 2026 11:56:27 +0200 Subject: [PATCH 3/8] update changelog and version label Signed-off-by: DONNOT Benjamin --- CHANGELOG.rst | 4 ++++ docs/conf.py | 2 +- grid2op/__init__.py | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 20f03d64a..83d15bcbe 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -100,6 +100,10 @@ Native multi agents support: - add detachment - add change_bus / set_bus +[1.12.5] 2026-xx-yy +-------------------- +- [FIXED] license issues in the documentation +- [IMPROVED] tests splitting to reduce the duration on circle ci [1.12.4] - 2026-04-28 ---------------------- diff --git a/docs/conf.py b/docs/conf.py index 082789cf0..76211ee49 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -22,7 +22,7 @@ author = 'Benjamin Donnot' # The full version, including alpha/beta/rc tags -release = '1.12.4' +release = '1.12.5.dev0' version = '1.12' diff --git a/grid2op/__init__.py b/grid2op/__init__.py index 16ed9ad9a..c7d404270 100644 --- a/grid2op/__init__.py +++ b/grid2op/__init__.py @@ -11,7 +11,7 @@ Grid2Op a testbed platform to model sequential decision making in power systems. """ -__version__ = '1.12.4' +__version__ = '1.12.5.dev0' __all__ = [ "Action", From bd1ecad77542dae1f58e40c9d21760e0eb9f241e Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Tue, 28 Apr 2026 13:51:47 +0200 Subject: [PATCH 4/8] avoid using assert in grid2op codebase Signed-off-by: DONNOT Benjamin --- grid2op/Chronics/fromNPY.py | 83 +++++++++++++------ grid2op/Converter/BackendConverter.py | 52 +++++++----- grid2op/Converter/IdToAct.py | 3 +- grid2op/Exceptions/__init__.py | 4 + grid2op/Exceptions/rewardExceptions.py | 18 ++++ grid2op/Reward/_alarmScore.py | 7 +- .../change_grid_params.py | 6 +- grid2op/gym_compat/gym_act_space.py | 6 +- 8 files changed, 123 insertions(+), 56 deletions(-) create mode 100644 grid2op/Exceptions/rewardExceptions.py diff --git a/grid2op/Chronics/fromNPY.py b/grid2op/Chronics/fromNPY.py index 7ebb2ac6c..3c7f03a88 100644 --- a/grid2op/Chronics/fromNPY.py +++ b/grid2op/Chronics/fromNPY.py @@ -6,7 +6,7 @@ # SPDX-License-Identifier: MPL-2.0 # This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. -from typing import Optional, Union, Dict, Literal +from typing import Optional, Union, Dict, Literal, TYPE_CHECKING import numpy as np import hashlib from datetime import datetime, timedelta @@ -16,6 +16,10 @@ from grid2op.Chronics.gridValue import GridValue from grid2op.Exceptions import ChronicsError +if TYPE_CHECKING: + from grid2op.Backend import Backend + from grid2op.Action import PlayableAction + class FromNPY(GridValue): """ @@ -169,7 +173,9 @@ def __init__( if maintenance is not None: self.has_maintenance = True self.n_line = maintenance.shape[1] - assert load_p.shape[0] == maintenance.shape[0] + if load_p.shape[0] != maintenance.shape[0]: + raise ChronicsError("The maintenance file does not have the same number " + "of time step at the load file") self.maintenance = maintenance # TODO copy self.maintenance_time = ( @@ -198,8 +204,11 @@ def __init__( self._forecasts = None if load_p_forecast is not None: - assert load_q_forecast is not None - assert prod_p_forecast is not None + if load_q_forecast is None: + raise ChronicsError("If you provide load_p_forecast you should provide load_q_forecast.") + if prod_p_forecast is None: + raise ChronicsError("If you provide load_p_forecast you should provide prod_p_forecast.") + self._forecasts = FromNPY( load_p=load_p_forecast, load_q=load_q_forecast, @@ -232,12 +241,17 @@ def initialize( order_backend_subs, names_chronics_to_backend=None, ): - assert len(order_backend_prods) == self.n_gen, f"len(order_backend_prods)={len(order_backend_prods)} vs self.n_gen={self.n_gen}" - assert len(order_backend_loads) == self.n_load, f"len(order_backend_loads)={len(order_backend_loads)} vs self.n_load={self.n_load}" + if len(order_backend_prods) != self.n_gen: + raise ChronicsError(f"len(order_backend_prods)={len(order_backend_prods)} vs self.n_gen={self.n_gen}") + + if len(order_backend_loads) != self.n_load: + raise ChronicsError(f"len(order_backend_loads)={len(order_backend_loads)} vs self.n_load={self.n_load}") + if self.n_line is None: self.n_line = len(order_backend_lines) else: - assert len(order_backend_lines) == self.n_line, f"len(order_backend_lines)={len(order_backend_lines)} vs self.n_line={self.n_line}" + if len(order_backend_lines) != self.n_line: + raise ChronicsError(f"len(order_backend_lines)={len(order_backend_lines)} vs self.n_line={self.n_line}") if self._forecasts is not None: self._forecasts.initialize( @@ -255,7 +269,7 @@ def initialize( self.current_index = self._i_start - 1 self._max_iter = self._i_end - self._i_start - def _get_long_hash(self, hash_: hashlib.blake2b = None): + def _get_long_hash(self, hash_: Optional[hashlib.blake2b] = None): # get the "long hash" from blake2b if hash_ is None: hash_ = ( @@ -361,33 +375,50 @@ def load_next(self): ) def check_validity( - self, backend: Optional["grid2op.Backend.backend.Backend"] + self, backend: Optional["Backend"] ) -> None: - # TODO raise the proper errors from ChronicsError here rather than AssertError - assert self._load_p.shape[0] == self._load_q.shape[0] - assert self._load_p.shape[0] == self._prod_p.shape[0] + if self._load_p.shape[0] != self._load_q.shape[0]: + raise ChronicsError("Loads P and Q must have the same number of timesteps / rows") + if self._load_p.shape[0] != self._prod_p.shape[0]: + raise ChronicsError("Loads P and gen p must have the same number of timesteps / rows") if self._prod_v is not None: - assert self._load_p.shape[0] == self._prod_v.shape[0] + if self._load_p.shape[0] != self._prod_v.shape[0]: + raise ChronicsError("Loads P and gen v (as it is provided) must have the same number of timesteps / rows") if self.hazards is not None: - assert self.hazards.shape[1] == self.n_line + if self.hazards.shape[1] != self.n_line: + raise ChronicsError("Loads P and hazards (as it is provided) must have the same number of timesteps / rows") + if self.maintenance is not None: - assert self.maintenance.shape[1] == self.n_line + if self.maintenance.shape[1] != self.n_line: + raise ChronicsError("Maintenance file (as it is provided) should have the " + "same number of columns as the number of powerlines on the grid") if self.maintenance_duration is not None: - assert self.n_line == self.maintenance_duration.shape[1] + if self.n_line != self.maintenance_duration.shape[1]: + raise ChronicsError("maintenance_duration file (as it is provided) should have the same " + "number of columns as the number of powerlines on the grid") if self.maintenance_time is not None: - assert self.n_line == self.maintenance_time.shape[1] + if self.n_line != self.maintenance_time.shape[1]: + raise ChronicsError("maintenance_time file (as it is provided) should have the same " + "number of columns as the number of powerlines on the grid") # TODO forecast if self._forecasts is not None: - assert self._forecasts.n_line == self.n_line - assert self._forecasts.n_gen == self.n_gen - assert self._forecasts.n_load == self.n_load - assert self._load_p.shape[0] == self._forecasts._load_p.shape[0] - assert self._load_q.shape[0] == self._forecasts._load_q.shape[0] - assert self._prod_p.shape[0] == self._forecasts._prod_p.shape[0] + if self._forecasts.n_line != self.n_line: + raise ChronicsError("self._forecasts.n_line != self.n_line") + if self._forecasts.n_gen != self.n_gen: + raise ChronicsError("self._forecasts.n_gen != self.n_gen") + if self._forecasts.n_load != self.n_load: + raise ChronicsError("self._forecasts.n_load != self.n_load") + if self._load_p.shape[0] != self._forecasts._load_p.shape[0]: + raise ChronicsError("self._load_p.shape[0] != self._forecasts._load_p.shape[0]") + if self._load_q.shape[0] != self._forecasts._load_q.shape[0]: + raise ChronicsError("self._load_q.shape[0] != self._forecasts._load_q.shape[0]") + if self._prod_p.shape[0] != self._forecasts._prod_p.shape[0]: + raise ChronicsError("self._prod_p.shape[0] != self._forecasts._prod_p.shape[0]") if self._prod_v is not None and self._forecasts._prod_v is not None: - assert self._prod_v.shape[0] == self._forecasts._prod_v.shape[0] + if self._prod_v.shape[0] == self._forecasts._prod_v.shape[0]: + raise ChronicsError("self._prod_v.shape[0] != self._forecasts._prod_v.shape[0]") self._forecasts.check_validity(backend=backend) def next_chronics(self): @@ -698,7 +729,9 @@ def change_i_end(self, new_i_end: Union[int, None]): else: self.__new_iend = None - def get_init_action(self, names_chronics_to_backend: Optional[Dict[Literal["loads", "prods", "lines"], Dict[str, str]]]=None) -> Union["grid2op.Action.playableAction.PlayableAction", None]: + def get_init_action(self, + names_chronics_to_backend: Optional[Dict[Literal["loads", "prods", "lines"], + Dict[str, str]]]=None) -> Union["PlayableAction", None]: # names_chronics_to_backend is ignored, names should be consistent between the environment # and the initial state diff --git a/grid2op/Converter/BackendConverter.py b/grid2op/Converter/BackendConverter.py index bf6c968cd..5d852a032 100644 --- a/grid2op/Converter/BackendConverter.py +++ b/grid2op/Converter/BackendConverter.py @@ -539,14 +539,16 @@ def assert_grid_correct(self, _local_dir_cls=None) -> None: if self.sub_source_target is None: # automatic mode for substations, names must match - assert np.all( + if ( self.target_backend.name_sub[self._sub_tg2sr] - == self.source_backend.name_sub - ) - assert np.all( + != self.source_backend.name_sub + ).any(): + raise BackendError("The names of the substations do not match") + if ( self.source_backend.name_sub[self._sub_sr2tg] - == self.target_backend.name_sub - ) + != self.target_backend.name_sub + ).any(): + raise BackendError("The names of the substations do not match") # check that all corresponding vectors are valid (and properly initialized, like every component above 0 etc.) self._check_both_consistent(self._line_tg2sr, self._line_sr2tg) @@ -561,15 +563,22 @@ def assert_grid_correct(self, _local_dir_cls=None) -> None: # n_storage == 0 and there are storage units on the source backend # this means that the target_backend supports storage but not # the source one - assert np.all(self._topo_sr2tg[self._topo_tg2sr] >= 0) - assert np.all(sorted(self._topo_sr2tg[self._topo_tg2sr]) == np.arange(self.dim_topo)) + if (self._topo_sr2tg[self._topo_tg2sr] < 0).any(): + raise BackendError("every topo element should be mapped some (self._topo_sr2tg[self._topo_tg2sr] < 0)") + if (sorted(self._topo_sr2tg[self._topo_tg2sr]) != np.arange(self.dim_topo)).any(): + raise BackendError("Some element of topology are not mapped") + topo_sr2tg_without_storage = self._topo_sr2tg[self._topo_sr2tg >= 0] - assert (self._topo_sr2tg == -1).sum() == tg_cls.n_storage - assert np.all(self._topo_tg2sr[topo_sr2tg_without_storage] >= 0) + if (self._topo_sr2tg == -1).sum() != tg_cls.n_storage: + raise BackendError("(self._topo_sr2tg == -1).sum() != tg_cls.n_storage") + + if (self._topo_tg2sr[topo_sr2tg_without_storage] < 0).any(): + raise BackendError("(self._topo_tg2sr[topo_sr2tg_without_storage] >= 0).any()") target_without_storage = np.array([i for i in range(tg_cls.dim_topo) if i not in tg_cls.storage_pos_topo_vect]) - assert np.all(sorted(self._topo_tg2sr[topo_sr2tg_without_storage]) == target_without_storage) + if (sorted(self._topo_tg2sr[topo_sr2tg_without_storage]) != target_without_storage).any(): + raise BackendError("(sorted(self._topo_tg2sr[topo_sr2tg_without_storage]) != target_without_storage).any()") self._topo_sr2tg = topo_sr2tg_without_storage if type(self).shunts_data_available: @@ -591,23 +600,22 @@ def assert_grid_correct(self, _local_dir_cls=None) -> None: self.names_target_to_source = dict_ def _check_vect_valid(self, vect): - assert np.all( - vect >= 0 - ), ERROR_INVALID_VECTOR - assert sorted(np.unique(vect)) == sorted( - vect - ), ERROR_INVALID_VECTOR + if np.any(vect < 0): + raise BackendError(ERROR_INVALID_VECTOR) + if sorted(np.unique(vect)) != sorted(vect): + raise BackendError(ERROR_INVALID_VECTOR) if vect.shape[0] > 0: - assert ( - np.max(vect) == vect.shape[0] - 1 - ), ERROR_INVALID_VECTOR + if np.max(vect) != vect.shape[0] - 1: + raise BackendError(ERROR_INVALID_VECTOR) def _check_both_consistent(self, tg2sr, sr2tg): self._check_vect_valid(tg2sr) self._check_vect_valid(sr2tg) res = np.arange(tg2sr.shape[0]) - assert np.all(tg2sr[sr2tg] == res) - assert np.all(sr2tg[tg2sr] == res) + if np.any(tg2sr[sr2tg] != res): + raise BackendError("np.any(tg2sr[sr2tg] != res)") + if np.any(sr2tg[tg2sr] != res): + raise BackendError("np.any(sr2tg[tg2sr] != res)") def assert_grid_correct_after_powerflow(self): # we don't assert that `self.source_backend.assert_grid_correct_after_powerflow()` diff --git a/grid2op/Converter/IdToAct.py b/grid2op/Converter/IdToAct.py index a06098a7f..7ab82e6ce 100644 --- a/grid2op/Converter/IdToAct.py +++ b/grid2op/Converter/IdToAct.py @@ -280,7 +280,8 @@ def init_converter(self, all_actions=None, **kwargs): nb = len(all_actions) # assert I can compute the "len" for i in range(nb): act = all_actions[i] # assert I can use the `[]` operator - assert isinstance(act, BaseAction) # assert what's in there is a BaseAction + if not isinstance(act, BaseAction): + raise RuntimeError("The action provided is not a valid grid2op action") except Exception as exc_: raise RuntimeError("Impossible to load the action provided.") from exc_ # does not copy here (to save memory in case of shared memory setting) diff --git a/grid2op/Exceptions/__init__.py b/grid2op/Exceptions/__init__.py index 03614f934..613ba101f 100644 --- a/grid2op/Exceptions/__init__.py +++ b/grid2op/Exceptions/__init__.py @@ -67,6 +67,7 @@ "SomeGeneratorBelowRampmin", "ImpossibleRedispatching", "InvalidBackendCallback", + "RewardException", ] from grid2op.Exceptions.grid2OpException import Grid2OpException @@ -156,3 +157,6 @@ from grid2op.Exceptions.agentError import AgentError from grid2op.Exceptions.simulatorExceptions import SimulatorError + +from grid2op.Exceptions.rewardExceptions import RewardException + diff --git a/grid2op/Exceptions/rewardExceptions.py b/grid2op/Exceptions/rewardExceptions.py new file mode 100644 index 000000000..3bad0dab0 --- /dev/null +++ b/grid2op/Exceptions/rewardExceptions.py @@ -0,0 +1,18 @@ +# Copyright (c) 2019-2022, RTE (https://www.rte-france.com) +# See AUTHORS.txt +# This Source Code Form is subject to the terms of the Mozilla Public License, version 2.0. +# If a copy of the Mozilla Public License, version 2.0 was not distributed with this file, +# you can obtain one at http://mozilla.org/MPL/2.0/. +# SPDX-License-Identifier: MPL-2.0 +# This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. + +from grid2op.Exceptions.grid2OpException import Grid2OpException + + +class RewardException(Grid2OpException): + """ + This exception indicate that the reward you are trying to use has + encounter an error. + """ + + pass diff --git a/grid2op/Reward/_alarmScore.py b/grid2op/Reward/_alarmScore.py index 82cc5a591..9725f76df 100644 --- a/grid2op/Reward/_alarmScore.py +++ b/grid2op/Reward/_alarmScore.py @@ -9,7 +9,7 @@ import numpy as np import copy -from grid2op.Exceptions import Grid2OpException +from grid2op.Exceptions import RewardException from grid2op.Reward import AlarmReward from grid2op.dtypes import dt_float @@ -69,7 +69,7 @@ def __init__(self, logger=None): def initialize(self, env): if not env._has_attention_budget: - raise Grid2OpException( + raise RewardException( 'Impossible to use the "_AlarmScore" with an environment for which this feature ' 'is disabled. Please make sure "env._has_attention_budget" is set to ``True`` or ' "change the reward class with `grid2op.make(..., reward_class=AnyOtherReward)`" @@ -105,7 +105,8 @@ def _lines_disconnected_first(self, disc_lines_at_cascading_time): disc_lines_to_consider_for_score = disc_lines_at_cascading_time == 0 # if we are there, it is because we have identified before that the failure is due to disconnected powerlines - assert (disc_lines_to_consider_for_score).any() + if (disc_lines_to_consider_for_score).all(): + raise RewardException("This reward should be called only if a line has been disconnected.") # we transform the vector so that disconnected lines have a zero, to be coherent with env._disc_lines return 1 - disc_lines_to_consider_for_score diff --git a/grid2op/data/l2rpn_case14_sandbox_diff_grid/change_grid_params.py b/grid2op/data/l2rpn_case14_sandbox_diff_grid/change_grid_params.py index 756a0ef4d..1b29d9eee 100644 --- a/grid2op/data/l2rpn_case14_sandbox_diff_grid/change_grid_params.py +++ b/grid2op/data/l2rpn_case14_sandbox_diff_grid/change_grid_params.py @@ -24,8 +24,10 @@ pp.runpp(sim_case) pp.runpp(real_case) -assert sim_case.converged -assert sim_case.res_line.shape[0] == sim_case.line.shape[0] +if not sim_case.converged: + raise RuntimeError("Should have converged") +if sim_case.res_line.shape[0] != sim_case.line.shape[0]: + raise RuntimeError("Should have the same number of lines") print(f"L1 error on p: {np.mean(np.abs(sim_case.res_line['p_from_mw'] - real_case.res_line['p_from_mw'])):.2f}MW") print(f"L1 error on q: {np.mean(np.abs(sim_case.res_line['q_from_mvar'] - real_case.res_line['q_from_mvar'])):.2f}MVAr") pp.to_json(sim_case, "grid_forecast.json") diff --git a/grid2op/gym_compat/gym_act_space.py b/grid2op/gym_compat/gym_act_space.py index 768d2951d..3a8f774ed 100644 --- a/grid2op/gym_compat/gym_act_space.py +++ b/grid2op/gym_compat/gym_act_space.py @@ -15,6 +15,7 @@ BaseMultiProcessEnvironment, ) from grid2op.Action import BaseAction, ActionSpace +from grid2op.Exceptions.grid2OpException import Grid2OpException from grid2op.dtypes import dt_int, dt_bool, dt_float from grid2op.Converter.Converters import Converter from grid2op.gym_compat.utils import GYM_AVAILABLE, GYMNASIUM_AVAILABLE, DictType @@ -368,9 +369,8 @@ def to_gym(self, action: object) -> DictType: gym_action = self._converter.convert_action_to_gym(action) else: # in that case action should be an instance of grid2op BaseAction - assert isinstance( - action, BaseAction - ), "impossible to convert an action not coming from grid2op" + if not isinstance(action, BaseAction): + raise Grid2OpException("impossible to convert an action not coming from grid2op") # TODO this do not work in case of multiple converter, # TODO this should somehow call tmp = self._keys_encoding[internal_k].g2op_to_gym(v) gym_action = self._base_to_gym( From 58b62b0d59492d201c93d583c6a320fcc37afdd4 Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Tue, 28 Apr 2026 16:50:33 +0200 Subject: [PATCH 5/8] exclude some other files from codacy - utils folder Signed-off-by: DONNOT Benjamin --- .codacy.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.codacy.yml b/.codacy.yml index 38dd3e42c..cb0c33df9 100644 --- a/.codacy.yml +++ b/.codacy.yml @@ -4,3 +4,4 @@ exclude_paths: - '*.md' - "examples/**" - "getting_started/**" + - utils/** From 4bbcfeb4b738425eeb59c991d416f10257f152ad Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Fri, 15 May 2026 10:55:36 +0200 Subject: [PATCH 6/8] trying to refactor the redispatching in the environment Signed-off-by: DONNOT Benjamin --- grid2op/Environment/baseEnv.py | 1173 +++++------------ grid2op/Environment/dispatch/__init__.py | 29 + .../dispatch/baseRedispatchSolver.py | 33 + .../Environment/dispatch/curtailmentModule.py | 67 + .../dispatch/defaultRedispatchSolver.py | 423 ++++++ .../Environment/dispatch/detachmentModule.py | 47 + grid2op/Environment/dispatch/dispatchTypes.py | 137 ++ .../Environment/dispatch/feasibilityGuard.py | 150 +++ grid2op/Environment/dispatch/storageModule.py | 109 ++ grid2op/Environment/environment.py | 2 + 10 files changed, 1315 insertions(+), 855 deletions(-) create mode 100644 grid2op/Environment/dispatch/__init__.py create mode 100644 grid2op/Environment/dispatch/baseRedispatchSolver.py create mode 100644 grid2op/Environment/dispatch/curtailmentModule.py create mode 100644 grid2op/Environment/dispatch/defaultRedispatchSolver.py create mode 100644 grid2op/Environment/dispatch/detachmentModule.py create mode 100644 grid2op/Environment/dispatch/dispatchTypes.py create mode 100644 grid2op/Environment/dispatch/feasibilityGuard.py create mode 100644 grid2op/Environment/dispatch/storageModule.py diff --git a/grid2op/Environment/baseEnv.py b/grid2op/Environment/baseEnv.py index 0716998d4..a559df79b 100644 --- a/grid2op/Environment/baseEnv.py +++ b/grid2op/Environment/baseEnv.py @@ -19,7 +19,6 @@ import warnings import numpy as np -from scipy.optimize import (minimize, LinearConstraint) from abc import ABC, abstractmethod from grid2op._glop_platform_info import _IS_WINDOWS @@ -59,6 +58,19 @@ from grid2op.Rules import AlwaysLegal, BaseRules from grid2op.typing_variables import STEP_INFO_TYPING, RESET_OPTIONS_TYPING from grid2op.VoltageControler import ControlVoltageFromFile +from grid2op.Environment.dispatch import ( + BaseRedispatchSolver, + CurtailmentModule, + CurtailmentResult, + DefaultRedispatchSolver, + DetachmentModule, + DetachmentResult, + FeasibilityGuard, + RedispatchConstraints, + RedispatchState, + StorageModule, + StorageResult, +) # TODO put in a separate class the redispatching function @@ -323,6 +335,7 @@ def foo(manager): ALARM_KEY = "fixed" ALERT_FILE_NAME = "alerts_info.json" ALERT_KEY = "by_line" + DETAILED_REDISP_ERR_MSG = DETAILED_REDISP_ERR_MSG CAN_SKIP_TS = False # each step is exactly one time step @@ -374,6 +387,7 @@ def __init__( _local_dir_cls=None, _read_from_local_dir=None, _raw_backend_class=None, + redispatch_solver: Optional[BaseRedispatchSolver] = None, ): #: flag to indicate not to erase the directory when the env has been used self._do_not_erase_local_dir_cls = False @@ -493,6 +507,7 @@ def __init__( self._hazard_duration: np.ndarray = None self._env_dc = self._parameters.ENV_DC + self._dispatch_state = RedispatchState() # redispatching data self._target_dispatch: np.ndarray = None @@ -658,6 +673,7 @@ def __init__( self._highres_sim_counter = HighResSimCounter() self._update_obs_after_reward = update_obs_after_reward + self._init_dispatch_modules(redispatch_solver) # alert self._last_alert = None @@ -696,7 +712,208 @@ def __init__( # 1.12.1 self._needs_active_bus = False - + + def _init_dispatch_modules( + self, + redispatch_solver: Optional[BaseRedispatchSolver], + ) -> None: + self._storage_module = StorageModule(self) + self._curtailment_module = CurtailmentModule(self) + self._detachment_module = DetachmentModule(self) + self._feasibility_guard = FeasibilityGuard(self) + if redispatch_solver is None: + solver = DefaultRedispatchSolver() + elif isinstance(redispatch_solver, type): + solver = redispatch_solver() + else: + solver = redispatch_solver + self._redispatch_solver = solver.bind(self) + + def _get_dispatch_attr(self, name: str): + return getattr(self._dispatch_state, name) + + def _set_dispatch_attr(self, name: str, value) -> None: + setattr(self._dispatch_state, name, value) + + def _set_dispatch_float_attr(self, name: str, value) -> None: + setattr(self._dispatch_state, name, dt_float(value)) + + @property + def _target_dispatch(self): + return self._get_dispatch_attr("target_dispatch") + + @_target_dispatch.setter + def _target_dispatch(self, value): + self._set_dispatch_attr("target_dispatch", value) + + @property + def _already_modified_gen(self): + return self._get_dispatch_attr("already_modified_gen") + + @_already_modified_gen.setter + def _already_modified_gen(self, value): + self._set_dispatch_attr("already_modified_gen", value) + + @property + def _actual_dispatch(self): + return self._get_dispatch_attr("actual_dispatch") + + @_actual_dispatch.setter + def _actual_dispatch(self, value): + self._set_dispatch_attr("actual_dispatch", value) + + @property + def _gen_uptime(self): + return self._get_dispatch_attr("gen_uptime") + + @_gen_uptime.setter + def _gen_uptime(self, value): + self._set_dispatch_attr("gen_uptime", value) + + @property + def _gen_downtime(self): + return self._get_dispatch_attr("gen_downtime") + + @_gen_downtime.setter + def _gen_downtime(self, value): + self._set_dispatch_attr("gen_downtime", value) + + @property + def _gen_activeprod_t(self): + return self._get_dispatch_attr("gen_activeprod_t") + + @_gen_activeprod_t.setter + def _gen_activeprod_t(self, value): + self._set_dispatch_attr("gen_activeprod_t", value) + + @property + def _gen_activeprod_t_redisp(self): + return self._get_dispatch_attr("gen_activeprod_t_redisp") + + @_gen_activeprod_t_redisp.setter + def _gen_activeprod_t_redisp(self, value): + self._set_dispatch_attr("gen_activeprod_t_redisp", value) + + @property + def _storage_current_charge(self): + return self._get_dispatch_attr("storage_current_charge") + + @_storage_current_charge.setter + def _storage_current_charge(self, value): + self._set_dispatch_attr("storage_current_charge", value) + + @property + def _storage_previous_charge(self): + return self._get_dispatch_attr("storage_previous_charge") + + @_storage_previous_charge.setter + def _storage_previous_charge(self, value): + self._set_dispatch_attr("storage_previous_charge", value) + + @property + def _action_storage(self): + return self._get_dispatch_attr("action_storage") + + @_action_storage.setter + def _action_storage(self, value): + self._set_dispatch_attr("action_storage", value) + + @property + def _amount_storage(self): + return self._get_dispatch_attr("amount_storage") + + @_amount_storage.setter + def _amount_storage(self, value): + self._set_dispatch_float_attr("amount_storage", value) + + @property + def _amount_storage_prev(self): + return self._get_dispatch_attr("amount_storage_prev") + + @_amount_storage_prev.setter + def _amount_storage_prev(self, value): + self._set_dispatch_float_attr("amount_storage_prev", value) + + @property + def _storage_power(self): + return self._get_dispatch_attr("storage_power") + + @_storage_power.setter + def _storage_power(self, value): + self._set_dispatch_attr("storage_power", value) + + @property + def _storage_power_prev(self): + return self._get_dispatch_attr("storage_power_prev") + + @_storage_power_prev.setter + def _storage_power_prev(self, value): + self._set_dispatch_attr("storage_power_prev", value) + + @property + def _limit_curtailment(self): + return self._get_dispatch_attr("limit_curtailment") + + @_limit_curtailment.setter + def _limit_curtailment(self, value): + self._set_dispatch_attr("limit_curtailment", value) + + @property + def _limit_curtailment_prev(self): + return self._get_dispatch_attr("limit_curtailment_prev") + + @_limit_curtailment_prev.setter + def _limit_curtailment_prev(self, value): + self._set_dispatch_attr("limit_curtailment_prev", value) + + @property + def _gen_before_curtailment(self): + return self._get_dispatch_attr("gen_before_curtailment") + + @_gen_before_curtailment.setter + def _gen_before_curtailment(self, value): + self._set_dispatch_attr("gen_before_curtailment", value) + + @property + def _sum_curtailment_mw(self): + return self._get_dispatch_attr("sum_curtailment_mw") + + @_sum_curtailment_mw.setter + def _sum_curtailment_mw(self, value): + self._set_dispatch_float_attr("sum_curtailment_mw", value) + + @property + def _sum_curtailment_mw_prev(self): + return self._get_dispatch_attr("sum_curtailment_mw_prev") + + @_sum_curtailment_mw_prev.setter + def _sum_curtailment_mw_prev(self, value): + self._set_dispatch_float_attr("sum_curtailment_mw_prev", value) + + @property + def _detached_elements_mw(self): + return self._get_dispatch_attr("detached_elements_mw") + + @_detached_elements_mw.setter + def _detached_elements_mw(self, value): + self._set_dispatch_float_attr("detached_elements_mw", value) + + @property + def _detached_elements_mw_prev(self): + return self._get_dispatch_attr("detached_elements_mw_prev") + + @_detached_elements_mw_prev.setter + def _detached_elements_mw_prev(self, value): + self._set_dispatch_float_attr("detached_elements_mw_prev", value) + + @property + def _limited_before(self): + return self._get_dispatch_attr("limited_before") + + @_limited_before.setter + def _limited_before(self, value): + self._set_dispatch_float_attr("limited_before", value) + @property def highres_sim_counter(self) -> int: return self._highres_sim_counter @@ -718,6 +935,7 @@ def _custom_deepcopy_for_copy(self, new_obj, dict_=None): RandomObject._custom_deepcopy_for_copy(self, new_obj) new_obj.name = self.name + new_obj._dispatch_state = RedispatchState() if dict_ is None: dict_ = {} new_obj._n_busbar = self._n_busbar @@ -1026,6 +1244,7 @@ def _custom_deepcopy_for_copy(self, new_obj, dict_=None): new_obj._called_from_reset = self._called_from_reset new_obj._needs_active_bus = self._needs_active_bus + new_obj._init_dispatch_modules(self._redispatch_solver.copy_for_env(new_obj)) def get_path_env(self): """ @@ -1421,14 +1640,7 @@ def _has_been_initialized(self): shape=(bk_type.n_line,), fill_value=0, dtype=dt_int ) - # create the vector to the proper shape - self._target_dispatch = np.zeros(bk_type.n_gen, dtype=dt_float) - self._already_modified_gen = np.zeros(bk_type.n_gen, dtype=dt_bool) - self._actual_dispatch = np.zeros(bk_type.n_gen, dtype=dt_float) - self._gen_uptime = np.zeros(bk_type.n_gen, dtype=dt_int) - self._gen_downtime = np.zeros(bk_type.n_gen, dtype=dt_int) - self._gen_activeprod_t = np.zeros(bk_type.n_gen, dtype=dt_float) - self._gen_activeprod_t_redisp = np.zeros(bk_type.n_gen, dtype=dt_float) + self._dispatch_state = RedispatchState.allocate(bk_type.n_gen, bk_type.n_storage) self._max_timestep_line_status_deactivated = ( self._parameters.NB_TIMESTEP_COOLDOWN_LINE ) @@ -1458,27 +1670,6 @@ def _has_been_initialized(self): self._reset_redispatching() - # storage - self._storage_current_charge = np.zeros(bk_type.n_storage, dtype=dt_float) - self._storage_previous_charge = np.zeros(bk_type.n_storage, dtype=dt_float) - self._action_storage = np.zeros(bk_type.n_storage, dtype=dt_float) - self._storage_power = np.zeros(bk_type.n_storage, dtype=dt_float) - self._storage_power_prev = np.zeros(bk_type.n_storage, dtype=dt_float) - self._amount_storage = 0.0 - self._amount_storage_prev = 0.0 - - # curtailment - self._limit_curtailment = np.ones( - bk_type.n_gen, dtype=dt_float - ) # in ratio of pmax - self._limit_curtailment_prev = np.ones( - bk_type.n_gen, dtype=dt_float - ) # in ratio of pmax - self._gen_before_curtailment = np.zeros(bk_type.n_gen, dtype=dt_float) # in MW - self._sum_curtailment_mw = dt_float(0.0) - self._sum_curtailment_mw_prev = dt_float(0.0) - self._detached_elements_mw = dt_float(0.0) - self._detached_elements_mw_prev = dt_float(0.0) self._reset_curtailment() # register this is properly initialized @@ -1686,27 +1877,10 @@ def _reset_alert(self): def _reset_storage(self): """reset storage capacity at the beginning of new environment if needed""" - if self.n_storage > 0: - tmp = self._parameters.INIT_STORAGE_CAPACITY * self.storage_Emax - if self._parameters.ACTIVATE_STORAGE_LOSS: - tmp += self.storage_loss * self.delta_time_seconds / 3600.0 - self._storage_previous_charge[ - : - ] = tmp # might not be needed, but it's not for the time it takes... - self._storage_current_charge[:] = tmp - self._storage_power[:] = 0.0 - self._storage_power_prev[:] = 0.0 - self._amount_storage = 0.0 - self._amount_storage_prev = 0.0 - # TODO storage: check in simulate too! + self._storage_module.reset(self._dispatch_state) def _reset_curtailment(self): - self._limit_curtailment[self.gen_renewable] = 1.0 - self._limit_curtailment_prev[self.gen_renewable] = 1.0 - self._gen_before_curtailment[:] = 0.0 - self._sum_curtailment_mw = dt_float(0.0) - self._sum_curtailment_mw_prev = dt_float(0.0) - self._limited_before = dt_float(0.0) + self._curtailment_module.reset(self._dispatch_state) def seed(self, seed=None, _seed_me=True): """ @@ -2058,21 +2232,11 @@ def set_thermal_limit(self, thermal_limit): self.observation_space.set_thermal_limit(self._thermal_limit_a) def _reset_redispatching(self): - # redispatching - self._target_dispatch[:] = 0.0 - self._already_modified_gen[:] = False - self._actual_dispatch[:] = 0.0 - self._gen_uptime[:] = 0 - self._gen_downtime[:] = 0 - self._gen_activeprod_t[:] = 0.0 - self._gen_activeprod_t_redisp[:] = 0.0 + self._redispatch_solver.reset(self._dispatch_state) def _feed_data_for_detachment(self, new_p_th): """feed the attribute for the detachment""" - - self._prev_gen_p[:] = new_p_th - self._aux_retrieve_modif_act(self._prev_load_p, self._env_modification, "load_p") - self._aux_retrieve_modif_act(self._prev_load_q, self._env_modification, "load_q") + self._detachment_module.feed_data(new_p_th, self._dispatch_state) def _aux_retrieve_modif_act(self, input_ : np.ndarray, @@ -2099,419 +2263,35 @@ def _get_new_prod_setpoint(self, action): return new_p def _get_already_modified_gen(self, action: BaseAction): - if not action._modif_redispatch: - # nothing changes if the action does - # not affect redispatching - return self._already_modified_gen - - redisp_act_orig = action._redispatch - is_redisped = np.abs(redisp_act_orig) > 1e-7 - self._target_dispatch[self._already_modified_gen] += redisp_act_orig[self._already_modified_gen] - first_modified = (~self._already_modified_gen) & is_redisped - self._target_dispatch[first_modified] = ( - self._actual_dispatch[first_modified] + redisp_act_orig[first_modified] - ) - self._already_modified_gen[is_redisped] = True - return self._already_modified_gen + return self._redispatch_solver._update_target_dispatch(action, self._dispatch_state) def _prepare_redisp(self, action: BaseAction, new_p, already_modified_gen): - cls = type(self) - # trying with an optimization method - except_ = None - info_ = [] - valid = True - - # get the redispatching action (if any) - if action._modif_redispatch: - redisp_act_orig = action._redispatch.copy() - else: - redisp_act_orig = None - - if ( - (redisp_act_orig is not None and (np.abs(redisp_act_orig) <= 1e-7).all()) - and (np.abs(self._target_dispatch) <= 1e-7).all() - and (np.abs(self._actual_dispatch) <= 1e-7).all() - ): - return valid, except_, info_ - - if redisp_act_orig is None: - redisp_act_orig = type(action)._build_attr("_redispatch") - - # check that everything is consistent with pmin, pmax: - if (self._target_dispatch > cls.gen_pmax - cls.gen_pmin).any(): - # action is invalid, the target redispatching would be above pmax for at least a generator - cond_invalid = self._target_dispatch > cls.gen_pmax - cls.gen_pmin - except_ = IllegalRedispatching( - "You cannot ask for a dispatch higher than pmax - pmin [it would be always " - "invalid because, even if the sepoint is pmin, this dispatch would set it " - "to a number higher than pmax, which is impossible]. Invalid dispatch for " - "generator(s): " - "{}".format((cond_invalid).nonzero()[0]) - ) - self._target_dispatch -= redisp_act_orig - return valid, except_, info_ - if (self._target_dispatch < cls.gen_pmin - cls.gen_pmax).any(): - # action is invalid, the target redispatching would be below pmin for at least a generator - cond_invalid = self._target_dispatch < cls.gen_pmin - cls.gen_pmax - except_ = IllegalRedispatching( - "You cannot ask for a dispatch lower than pmin - pmax [it would be always " - "invalid because, even if the sepoint is pmax, this dispatch would set it " - "to a number bellow pmin, which is impossible]. Invalid dispatch for " - "generator(s): " - "{}".format((cond_invalid).nonzero()[0]) - ) - self._target_dispatch -= redisp_act_orig - return valid, except_, info_ - - # i can't redispatch turned off generators [turned off generators need to be turned on before redispatching] - if (redisp_act_orig[np.abs(new_p) <= 1e-7]).any() and self._forbid_dispatch_off: - # action is invalid, a generator has been redispatched, but it's turned off - except_ = IllegalRedispatching( - "Impossible to dispatch a turned off generator" - ) - self._target_dispatch -= redisp_act_orig - return valid, except_, info_ - - if self._forbid_dispatch_off: - redisp_act_orig_cut = redisp_act_orig.copy() - redisp_act_orig_cut[np.abs(new_p) <= 1e-7] = 0.0 - if (redisp_act_orig_cut != redisp_act_orig).any(): - info_.append( - { - "INFO: redispatching cut because generator will be turned_off": ( - redisp_act_orig_cut != redisp_act_orig - ).nonzero()[0] - } - ) - return valid, except_, info_ + return self._redispatch_solver._validate_dispatch( + action, new_p, already_modified_gen, self._dispatch_state + ) def _make_redisp(self, already_modified_gen, new_p): """this computes the redispaching vector, taking into account the storage units""" - except_ = None - valid = True - if not self._parameters.ENV_DOES_REDISPATCHING: - # env redispatching routine is asked not - # to work - self._actual_dispatch[:] = self._target_dispatch.copy() - return valid, except_ - - mismatch = self._actual_dispatch - self._target_dispatch - mismatch = np.abs(mismatch) - if ( - np.abs((self._actual_dispatch).sum()) >= self._tol_poly - or np.max(mismatch) >= self._tol_poly - or np.abs(self._amount_storage) >= self._tol_poly - or np.abs(self._sum_curtailment_mw) >= self._tol_poly - or np.abs(self._detached_elements_mw) >= self._tol_poly - ): - except_ = self._compute_dispatch_vect(already_modified_gen, new_p) - valid = except_ is None - return valid, except_ - - def _compute_dispatch_vect(self, already_modified_gen, new_p): - except_ = None - cls = type(self) - this_dt_float = float # to be compliant with scipy 1.16 (removes np.float32) - # handle the case where there are storage or redispatching - # action or curtailment action on the "init state" - # of the grid - if self.nb_time_step == 0: - self._gen_activeprod_t_redisp[:] = new_p - - # first i define the participating generators - # these are the generators that will be adjusted for redispatching - gen_participating = ( - (new_p > 0.0) - | (np.abs(self._actual_dispatch) >= 1e-7) - | (self._target_dispatch != self._actual_dispatch) - ) - gen_participating[~cls.gen_redispatchable] = False - if cls.detachment_is_allowed: - gen_participating[self._backend_action.get_gen_detached()] = False - incr_in_chronics = new_p - ( - self._gen_activeprod_t_redisp - self._actual_dispatch + constraints = RedispatchConstraints.from_results( + None, None, None, new_p, self._dispatch_state, self ) + except_ = self._redispatch_solver.solve(constraints, self._dispatch_state) + return except_ is None, except_ - # check if the constraints are violated - ## total available "juice" to go down (incl ramp and pmin / pmax) - p_min_down = ( - cls.gen_pmin[gen_participating] - - self._gen_activeprod_t_redisp[gen_participating] - ) - avail_down = np.maximum(p_min_down, -cls.gen_max_ramp_down[gen_participating]) - ## total available "juice" to go up (incl. ramp and pmin / pmax) - p_max_up = ( - cls.gen_pmax[gen_participating] - - self._gen_activeprod_t_redisp[gen_participating] - ) - avail_up = np.minimum(p_max_up, cls.gen_max_ramp_up[gen_participating]) - except_ = self._detect_infeasible_dispatch( - incr_in_chronics[gen_participating], avail_down, avail_up - ) - if except_ is not None: - # try to force the turn on of turned off generators (if parameters allow it) - if ( - self._parameters.IGNORE_MIN_UP_DOWN_TIME - and self._parameters.ALLOW_DISPATCH_GEN_SWITCH_OFF - ): - gen_participating_tmp = self.gen_redispatchable - if cls.detachment_is_allowed: - gen_participating_tmp[self._backend_action.get_gen_detached()] = False - p_min_down_tmp = ( - cls.gen_pmin[gen_participating_tmp] - - self._gen_activeprod_t_redisp[gen_participating_tmp] - ) - avail_down_tmp = np.maximum( - p_min_down_tmp, -cls.gen_max_ramp_down[gen_participating_tmp] - ) - p_max_up_tmp = ( - cls.gen_pmax[gen_participating_tmp] - - self._gen_activeprod_t_redisp[gen_participating_tmp] - ) - avail_up_tmp = np.minimum( - p_max_up_tmp, cls.gen_max_ramp_up[gen_participating_tmp] - ) - except_tmp = self._detect_infeasible_dispatch( - incr_in_chronics[gen_participating_tmp], - avail_down_tmp, - avail_up_tmp, - ) - if except_tmp is None: - # I can "save" the situation by turning on all generators, I do it - # TODO logger here - gen_participating = gen_participating_tmp - except_ = None - else: - return except_tmp - else: - return except_ - - # define the objective value - target_vals = ( - self._target_dispatch[gen_participating] - - self._actual_dispatch[gen_participating] - ) - - already_modified_gen_me = already_modified_gen[gen_participating] - target_vals_me = target_vals[already_modified_gen_me] - nb_dispatchable = gen_participating.sum() - tmp_zeros = np.zeros((1, nb_dispatchable), dtype=this_dt_float) - coeffs = 1.0 / ( - self.gen_max_ramp_up + self.gen_max_ramp_down + self._epsilon_poly - ) - weights = np.ones(nb_dispatchable) * coeffs[gen_participating] - weights /= weights.sum() - - if target_vals_me.shape[0] == 0: - # no dispatch means all dispatchable, otherwise i will never get to 0 - already_modified_gen_me[:] = True - target_vals_me = target_vals[already_modified_gen_me] - - # for numeric stability - # to scale the input also: - # see https://stackoverflow.com/questions/11155721/positive-directional-derivative-for-linesearch - scale_x = max(np.max(np.abs(self._actual_dispatch)), 1.0) - scale_x = this_dt_float(scale_x) - target_vals_me_optim = 1.0 * (target_vals_me / scale_x) - target_vals_me_optim = target_vals_me_optim.astype(this_dt_float) - - # see https://stackoverflow.com/questions/11155721/positive-directional-derivative-for-linesearch - # where they advised to scale the function - scale_objective = max(0.5 * np.abs(target_vals_me_optim).sum() ** 2, 1.0) - scale_objective = np.round(scale_objective, decimals=4) - scale_objective = this_dt_float(scale_objective) - - # add the "sum to 0" - mat_sum_0_no_turn_on = np.ones((1, nb_dispatchable), dtype=this_dt_float) - # this is where the storage is taken into account - # storages are "load convention" this means that i need to sum the amount of production to sum of storage - # hence the "+ self._amount_storage" below - # self._sum_curtailment_mw is "generator convention" hence the "-" there - const_sum_0_no_turn_on = ( - np.zeros(1, dtype=this_dt_float) - + self._amount_storage - - self._sum_curtailment_mw - + self._detached_elements_mw - ) - - # gen increase in the chronics - new_p_th = new_p[gen_participating] + self._actual_dispatch[gen_participating] - - # minimum value available for disp - ## first limit delta because of pmin - p_min_const = self.gen_pmin[gen_participating] - new_p_th - ## second limit delta because of ramps - ramp_down_const = ( - -self.gen_max_ramp_down[gen_participating] - - incr_in_chronics[gen_participating] - ) - ## take max of the 2 - min_disp = np.maximum(p_min_const, ramp_down_const) - min_disp = min_disp.astype(this_dt_float) - - # maximum value available for disp - ## first limit delta because of pmin - p_max_const = self.gen_pmax[gen_participating] - new_p_th - ## second limit delta because of ramps - ramp_up_const = ( - self.gen_max_ramp_up[gen_participating] - - incr_in_chronics[gen_participating] - ) - ## take min of the 2 - max_disp = np.minimum(p_max_const, ramp_up_const) - max_disp = max_disp.astype(this_dt_float) - - # add everything into a linear constraint object - # equality - added = 0.5 * self._epsilon_poly - equality_const = LinearConstraint( - mat_sum_0_no_turn_on, # do the sum - (const_sum_0_no_turn_on) / scale_x, # lower bound - (const_sum_0_no_turn_on) / scale_x, # upper bound - ) - mat_pmin_max_ramps = np.eye(nb_dispatchable) - ineq_const = LinearConstraint( - mat_pmin_max_ramps, - (min_disp - added) / scale_x, - (max_disp + added) / scale_x, + def _compute_dispatch_vect(self, already_modified_gen, new_p): + constraints = RedispatchConstraints.from_results( + None, None, None, new_p, self._dispatch_state, self ) - - # choose a good initial point (close to the solution) - # the idea here is to chose a initial point that would be close to the - # desired solution (split the (sum of the) dispatch to the available generators) - x0 = np.zeros(gen_participating.sum(), dtype=this_dt_float) - if (np.abs(self._target_dispatch) >= 1e-7).any() or already_modified_gen.any(): - gen_for_x0 = np.abs(self._target_dispatch[gen_participating]) >= 1e-7 - gen_for_x0 |= already_modified_gen[gen_participating] - x0[gen_for_x0] = ( - self._target_dispatch[gen_participating][gen_for_x0] - - self._actual_dispatch[gen_participating][gen_for_x0] - ) / scale_x - # at this point x0 is made of the difference between the target and the - # actual dispatch for all generators that have a - # target dispatch non 0. - - # in this "if" block I set the other component of x0 to - # their "right" value - can_adjust = (np.abs(x0) <= 1e-7) - if can_adjust.any(): - init_sum = x0.sum() - denom_adjust = (1.0 / weights[can_adjust]).sum() - if denom_adjust <= 1e-2: - # i don't want to divide by something too cloose to 0. - denom_adjust = 1.0 - x0[can_adjust] = -init_sum / (weights[can_adjust] * denom_adjust) - else: - # to "force" the exact reset to 0.0 for all components - x0 -= self._actual_dispatch[gen_participating] / scale_x - - def target(actual_dispatchable): - # define my real objective - quad_ = ( - actual_dispatchable[already_modified_gen_me] - target_vals_me_optim - ) ** 2 - coeffs_quads = weights[already_modified_gen_me] * quad_ - coeffs_quads_const = coeffs_quads.sum() - coeffs_quads_const /= scale_objective # scaling the function - return coeffs_quads_const - - def jac(actual_dispatchable): - res_jac = 1.0 * tmp_zeros - res_jac[0, already_modified_gen_me] = ( - 2.0 - * weights[already_modified_gen_me] - * (actual_dispatchable[already_modified_gen_me] - target_vals_me_optim) - ) - res_jac /= scale_objective # scaling the function - return res_jac.reshape(-1) - - # objective function - def f(init): - this_res = minimize( - target, - init, - method="SLSQP", - constraints=[equality_const, ineq_const], - options={ - "eps": max(this_dt_float(self._epsilon_poly / scale_x), 1e-6), - "ftol": max(this_dt_float(self._epsilon_poly / scale_x), 1e-6), - "disp": False, - }, - jac=jac - # hess=hess # not used for SLSQP - ) - return this_res - res = f(x0) - if res.success: - self._actual_dispatch[gen_participating] += res.x * scale_x - else: - # check if constraints are "approximately" met - mat_const = np.concatenate((mat_sum_0_no_turn_on, mat_pmin_max_ramps)) - downs = np.concatenate( - (const_sum_0_no_turn_on / scale_x, (min_disp - added) / scale_x) - ) - ups = np.concatenate( - (const_sum_0_no_turn_on / scale_x, (max_disp + added) / scale_x) - ) - vals = np.matmul(mat_const, res.x) - ok_down = np.all( - vals - downs >= -self._tol_poly - ) # i don't violate "down" constraints - ok_up = np.all(vals - ups <= self._tol_poly) - if ok_up and ok_down: - # it's ok i can tolerate "small" perturbations - self._actual_dispatch[gen_participating] += res.x * scale_x - else: - # TODO try with another method here, maybe - error_dispatch = ( - "Redispatching automaton terminated with error (no more information available " - 'at this point):\n"{}"'.format(res.message) - ) - except_ = ImpossibleRedispatching(error_dispatch) - return except_ + return self._redispatch_solver._solve(constraints, self._dispatch_state) def _detect_infeasible_dispatch(self, incr_in_chronics, avail_down, avail_up): """This function is an attempt to give more detailed log by detecting infeasible dispatch""" - except_ = None - sum_move = ( - incr_in_chronics.sum() + self._amount_storage - self._sum_curtailment_mw + self._detached_elements_mw + constraints = RedispatchConstraints.from_results( + None, None, None, self._gen_activeprod_t, self._dispatch_state, self + ) + return self._redispatch_solver._detect_infeasible_dispatch( + constraints, incr_in_chronics, avail_down, avail_up, self._dispatch_state ) - avail_down_sum = avail_down.sum() - avail_up_sum = avail_up.sum() - gen_setpoint = self._gen_activeprod_t_redisp[self.gen_redispatchable] - if sum_move > avail_up_sum: - # infeasible because too much is asked - msg = DETAILED_REDISP_ERR_MSG.format( - sum_move=sum_move, - avail_up_sum=avail_up_sum, - gen_setpoint=np.round(gen_setpoint, decimals=2), - ramp_up=self.gen_max_ramp_up[self.gen_redispatchable], - gen_pmax=self.gen_pmax[self.gen_redispatchable], - avail_up=np.round(avail_up, decimals=2), - increase="increase", - decrease="decrease", - maximum="maximum", - pmax="pmax", - max_ramp_up="max_ramp_up", - ) - except_ = ImpossibleRedispatching(msg) - elif sum_move < avail_down_sum: - # infeasible because not enough is asked - msg = DETAILED_REDISP_ERR_MSG.format( - sum_move=sum_move, - avail_up_sum=avail_down_sum, - gen_setpoint=np.round(gen_setpoint, decimals=2), - ramp_up=self.gen_max_ramp_down[self.gen_redispatchable], - gen_pmax=self.gen_pmin[self.gen_redispatchable], - avail_up=np.round(avail_up, decimals=2), - increase="decrease", - decrease="increase", - maximum="minimum", - pmax="pmin", - max_ramp_up="max_ramp_down", - ) - except_ = ImpossibleRedispatching(msg) - return except_ def _update_actions(self): """ @@ -2629,68 +2409,9 @@ def _handle_updown_times(self, gen_up_before, redisp_act): Handles the up and down tims for the generators. """ - # get the generators that are not connected after the action - except_ = None - cls = type(self) - # computes which generator will be turned on after the action - gen_up_after = self._gen_activeprod_t.copy() - if "prod_p" in self._env_modification._dict_inj: - tmp = self._env_modification._dict_inj["prod_p"] - indx_ok = np.isfinite(tmp) - gen_up_after[indx_ok] = self._env_modification._dict_inj["prod_p"][indx_ok] - gen_up_after += redisp_act - gen_up_after = np.abs(gen_up_after) > 1e-7 - - # update min down time, min up time etc. - gen_disconnected_this = gen_up_before & (~gen_up_after) - gen_connected_this_timestep = (~gen_up_before) & (gen_up_after) & ~self._gens_detached - gen_still_connected = (gen_up_before & gen_up_after) - gen_still_disconnected = ((~gen_up_before) & (~gen_up_after)) | self._gens_detached - if (not self._ignore_min_up_down_times and - ( - self._gen_downtime[gen_connected_this_timestep] - < cls.gen_min_downtime[gen_connected_this_timestep] - ).any() - ): - # i reconnected a generator before the minimum time allowed - id_gen = ( - self._gen_downtime[gen_connected_this_timestep] - < cls.gen_min_downtime[gen_connected_this_timestep] - ) - id_gen = (id_gen).nonzero()[0] - id_gen = (gen_connected_this_timestep[id_gen]).nonzero()[0] - except_ = GeneratorTurnedOnTooSoon( - "Some generator has been connected too early ({})".format(id_gen) - ) - return except_ - else: - self._gen_downtime[gen_connected_this_timestep] = -1 - self._gen_uptime[gen_connected_this_timestep] = 0 - - if (not self._ignore_min_up_down_times and - ( - self._gen_uptime[gen_disconnected_this] - < cls.gen_min_uptime[gen_disconnected_this] - ).any() - ): - # i disconnected a generator before the minimum time allowed - id_gen = ( - self._gen_uptime[gen_disconnected_this] - < cls.gen_min_uptime[gen_disconnected_this] - ) - id_gen = (id_gen).nonzero()[0] - id_gen = (gen_disconnected_this[id_gen]).nonzero()[0] - except_ = GeneratorTurnedOffTooSoon( - "Some generator has been disconnected too early ({})".format(id_gen) - ) - return except_ - else: - self._gen_downtime[gen_disconnected_this] = 0 - self._gen_uptime[gen_disconnected_this] = -1 - - self._gen_uptime[gen_still_connected] += 1 - self._gen_downtime[gen_still_disconnected] += 1 - return except_ + return self._redispatch_solver._check_updown_times( + gen_up_before, redisp_act, self._dispatch_state + ) def get_obs(self, _update_state=True, _do_copy=True): """ @@ -2780,15 +2501,7 @@ def _withdraw_storage_losses(self): NB this is a loss, this is not seen grid side, so `storage_discharging_efficiency` has no impact on this """ - # NB this should be done AFTER the computation of self._amount_storage, because this energy is dissipated - # in the storage units, thus NOT seen as power from the grid. - if self._parameters.ACTIVATE_STORAGE_LOSS: - tmp_ = self.storage_loss * self.delta_time_seconds / 3600.0 - self._storage_current_charge -= tmp_ - # charge cannot be negative, but it can be below Emin if there are some uncompensated losses - self._storage_current_charge[:] = np.maximum( - self._storage_current_charge, 0.0 - ) + self._storage_module.withdraw_losses(self._dispatch_state) def _aux_remove_power_too_high(self, delta_, indx_too_high): """ @@ -2797,16 +2510,7 @@ def _aux_remove_power_too_high(self, delta_, indx_too_high): handles self._storage_power in case we need to cut the storage action because the power would be too high """ - coeff_p_to_E = ( - self.delta_time_seconds / 3600.0 - ) # TODO optim this is const for all time steps - tmp_ = 1.0 / coeff_p_to_E * delta_ - if self._parameters.ACTIVATE_STORAGE_LOSS: - # from the storage i need to reduce of tmp_ MW (to compensate the delta_ MWh) - # but when it's "transfer" to the grid i don't have the same amount (due to inefficiencies) - # it's a "/" because i need more energy from the grid than what the actual charge will be - tmp_ /= self.storage_charging_efficiency[indx_too_high] - self._storage_power[indx_too_high] -= tmp_ + self._storage_module._clamp_too_high(delta_, indx_too_high, self._dispatch_state) def _aux_remove_power_too_low(self, delta_, indx_too_low): """ @@ -2815,94 +2519,10 @@ def _aux_remove_power_too_low(self, delta_, indx_too_low): handles self._storage_power in case we need to cut the storage action because the power would be too low """ - coeff_p_to_E = ( - self.delta_time_seconds / 3600.0 - ) # TODO optim this is const for all time steps - tmp_ = 1.0 / coeff_p_to_E * delta_ - if self._parameters.ACTIVATE_STORAGE_LOSS: - # from the storage i need to increase of tmp_ MW (to compensate the delta_ MWh) - # but when it's "transfer" to the grid i don't have the same amount (due to inefficiencies) - # it's a "*" because i have less power on the grid than what is removed from the battery - tmp_ *= self.storage_discharging_efficiency[indx_too_low] - self._storage_power[indx_too_low] -= tmp_ + self._storage_module._clamp_too_low(delta_, indx_too_low, self._dispatch_state) def _compute_storage(self, action_storage_power): - self._storage_previous_charge[:] = self._storage_current_charge - storage_act = np.isfinite(action_storage_power) & (np.abs(action_storage_power) >= 1e-7) - self._action_storage[:] = 0.0 - self._storage_power[:] = 0.0 - modif = False - coeff_p_to_E = ( - self.delta_time_seconds / 3600.0 - ) # TODO optim this is const for all time steps - if storage_act.any(): - modif = True - this_act_stor = action_storage_power[storage_act] - eff_ = np.ones(storage_act.sum()) - if self._parameters.ACTIVATE_STORAGE_LOSS: - fill_storage = ( - this_act_stor > 0.0 - ) # index of storages that sees their charge increasing - unfill_storage = ( - this_act_stor < 0.0 - ) # index of storages that sees their charge decreasing - eff_[fill_storage] *= self.storage_charging_efficiency[storage_act][ - fill_storage - ] - eff_[unfill_storage] /= self.storage_discharging_efficiency[ - storage_act - ][unfill_storage] - self._storage_current_charge[storage_act] += ( - this_act_stor * coeff_p_to_E * eff_ - ) - self._action_storage[storage_act] += action_storage_power[storage_act] - self._storage_power[storage_act] = this_act_stor - - if modif: - # indx when there is too much energy on the battery - indx_too_high = self._storage_current_charge > self.storage_Emax - if indx_too_high.any(): - delta_ = ( - self._storage_current_charge[indx_too_high] - - self.storage_Emax[indx_too_high] - ) - self._aux_remove_power_too_high(delta_, indx_too_high) - self._storage_current_charge[indx_too_high] = self.storage_Emax[ - indx_too_high - ] - - # indx when there is not enough energy on the battery - indx_too_low = self._storage_current_charge < self.storage_Emin - if indx_too_low.any(): - delta_ = ( - self._storage_current_charge[indx_too_low] - - self.storage_Emin[indx_too_low] - ) - self._aux_remove_power_too_low(delta_, indx_too_low) - self._storage_current_charge[indx_too_low] = self.storage_Emin[ - indx_too_low - ] - - self._storage_current_charge[:] = np.maximum( - self._storage_current_charge, self.storage_Emin - ) - # storage is "load convention", dispatch is "generator convention" - # i need the generator to have the same sign as the action on the batteries - self._amount_storage = self._storage_power.sum() - else: - # battery effect should be removed, so i multiply it by -1. - self._amount_storage = 0.0 - - tmp = self._amount_storage - self._amount_storage -= self._amount_storage_prev - self._amount_storage_prev = tmp - - # dissipated energy, it's not seen on the grid, just lost in the storage unit. - # this is why it should not be taken into account in self._amount_storage - # and NOT absorbed by the generators either - # NB loss in the storage unit can make it got below Emin in energy, but never below 0. - self._withdraw_storage_losses() - # end storage + return self._storage_module.compute(action_storage_power, self._dispatch_state) def _compute_max_ramp_this_step(self, new_p): """ @@ -2911,222 +2531,48 @@ def _compute_max_ramp_this_step(self, new_p): new_p: array of the (temporary) new production in the chronics that should happen """ - # TODO - # maximum value it can take - th_max = np.minimum( - self._gen_activeprod_t_redisp[self.gen_redispatchable] - + self.gen_max_ramp_up[self.gen_redispatchable], - self.gen_pmax[self.gen_redispatchable], - ) - # minimum value it can take - th_min = np.maximum( - self._gen_activeprod_t_redisp[self.gen_redispatchable] - - self.gen_max_ramp_down[self.gen_redispatchable], - self.gen_pmin[self.gen_redispatchable], - ) - - max_total_up = (th_max - new_p[self.gen_redispatchable]).sum() - max_total_down = ( - th_min - new_p[self.gen_redispatchable] - ).sum() # TODO is that it ? - return max_total_down, max_total_up + return self._feasibility_guard._compute_ramp_budget(new_p, self._dispatch_state) def _aux_update_curtail_env_act(self, new_p): - if "prod_p" in self._env_modification._dict_inj: - self._env_modification._dict_inj["prod_p"][:] = new_p - else: - self._env_modification._dict_inj["prod_p"] = 1.0 * new_p - self._env_modification._modif_inj = True + self._curtailment_module._update_env_action(new_p) def _aux_update_curtailment_act(self, action): - curtailment_act = 1.0 * action._curtail - ind_curtailed_in_act = (curtailment_act != -1.0) & self.gen_renewable - self._limit_curtailment_prev[:] = self._limit_curtailment - self._limit_curtailment[ind_curtailed_in_act] = curtailment_act[ - ind_curtailed_in_act - ] + self._curtailment_module._update_limits(action, self._dispatch_state) def _aux_compute_new_p_curtailment(self, new_p, curtailment_vect): """modifies the new_p argument !!!!""" - gen_curtailed = ( - np.abs(curtailment_vect - 1.) >= 1e-7 - ) # curtailed either right now, or in a previous action - max_action = self.gen_pmax[gen_curtailed] * curtailment_vect[gen_curtailed] - new_p[gen_curtailed] = np.minimum(max_action, new_p[gen_curtailed]) - return gen_curtailed + return self._curtailment_module._apply_limits(new_p, curtailment_vect) def _aux_handle_curtailment_without_limit(self, action, new_p): """modifies the new_p argument !!!! (but not the action)""" - if self.redispatching_unit_commitment_availble and ( - action._modif_curtailment or (np.abs(self._limit_curtailment - 1.) >= 1e-7).any() - ): - self._aux_update_curtailment_act(action) - - gen_curtailed = self._aux_compute_new_p_curtailment( - new_p, self._limit_curtailment - ) - - tmp_sum_curtailment_mw = dt_float( - new_p[gen_curtailed].sum() - - self._gen_before_curtailment[gen_curtailed].sum() - ) - - self._sum_curtailment_mw = ( - tmp_sum_curtailment_mw - self._sum_curtailment_mw_prev - ) - self._sum_curtailment_mw_prev = tmp_sum_curtailment_mw - self._aux_update_curtail_env_act(new_p) - else: - self._sum_curtailment_mw = -self._sum_curtailment_mw_prev - self._sum_curtailment_mw_prev = dt_float(0.0) - gen_curtailed = np.abs(self._limit_curtailment - 1.) >= 1e-7 - - return gen_curtailed + return self._curtailment_module.compute(action, new_p, self._dispatch_state) def _aux_readjust_curtailment_after_limiting( self, total_curtailment, new_p_th, new_p ): - self._sum_curtailment_mw += total_curtailment - self._sum_curtailment_mw_prev += total_curtailment - if total_curtailment > self._tol_poly: - # in this case, the curtailment is too strong, I need to make it less strong - curtailed = new_p_th - new_p - else: - # in this case, the curtailment is too low, this can happen, for example when there is a - # "strong" curtailment but afterwards you ask to set everything to 1. (so no curtailment) - # I cannot reuse the previous case (too_much > self._tol_poly) because the - # curtailment is already computed there... - new_p_with_previous_curtailment = 1.0 * new_p_th - self._aux_compute_new_p_curtailment( - new_p_with_previous_curtailment, self._limit_curtailment_prev - ) - curtailed = new_p_th - new_p_with_previous_curtailment - - curt_sum = curtailed.sum() - if abs(curt_sum) > self._tol_poly: - curtailed[~self.gen_renewable] = 0.0 - curtailed *= total_curtailment / curt_sum - new_p[self.gen_renewable] += curtailed[self.gen_renewable] + self._feasibility_guard._readjust_curtailment( + total_curtailment, new_p_th, new_p, self._dispatch_state + ) def _aux_readjust_storage_after_limiting(self, total_storage): - new_act_storage = 1.0 * self._storage_power - sum_this_step = new_act_storage.sum() - if abs(total_storage) < abs(sum_this_step): - # i can modify the current action - modif_storage = new_act_storage * total_storage / sum_this_step - else: - # i need to retrieve what I did in a previous action - # because the current action is not enough (the previous actions - # cause a problem right now) - new_act_storage = 1.0 * self._storage_power_prev - sum_this_step = new_act_storage.sum() - if abs(sum_this_step) > 1e-1: - modif_storage = new_act_storage * total_storage / sum_this_step - else: - # TODO: this is not cover by any test :-( - # it happens when you do an action too strong, then a do nothing, - # then you decrease the limit to rapidly - # (game over would jappen after at least one do nothing) - - # In this case I reset it completely or do I ? I don't really - # know what to do ! - modif_storage = new_act_storage # or self._storage_power ??? - - # handle self._storage_power and self._storage_current_charge - coeff_p_to_E = ( - self.delta_time_seconds / 3600.0 - ) # TODO optim this is const for all time steps - self._storage_power -= modif_storage - - # now compute the state of charge of the storage units (with efficiencies) - is_discharging = self._storage_power < 0.0 - is_charging = self._storage_power > 0.0 - modif_storage[is_discharging] /= type(self).storage_discharging_efficiency[ - is_discharging - ] - modif_storage[is_charging] *= type(self).storage_charging_efficiency[ - is_charging - ] - - self._storage_current_charge -= coeff_p_to_E * modif_storage - # inform the grid that the storage is reduced - self._amount_storage -= total_storage - self._amount_storage_prev -= total_storage + self._feasibility_guard._readjust_storage(total_storage, self._dispatch_state) def _aux_limit_curtail_storage_if_needed(self, new_p, new_p_th, gen_curtailed): - gen_redisp = self.gen_redispatchable - - normal_increase = new_p - ( - self._gen_activeprod_t_redisp - self._actual_dispatch + storage_result = StorageResult( + amount_storage_mw=self._amount_storage, + storage_power=self._storage_power, ) - normal_increase = normal_increase[gen_redisp] - p_min_down = ( - self.gen_pmin[gen_redisp] - self._gen_activeprod_t_redisp[gen_redisp] + curtail_result = CurtailmentResult( + sum_curtailment_mw=self._sum_curtailment_mw, + gen_curtailed=gen_curtailed, ) - avail_down = np.maximum(p_min_down, -self.gen_max_ramp_down[gen_redisp]) - p_max_up = self.gen_pmax[gen_redisp] - self._gen_activeprod_t_redisp[gen_redisp] - avail_up = np.minimum(p_max_up, self.gen_max_ramp_up[gen_redisp]) - - sum_move = ( - normal_increase.sum() + self._amount_storage - self._sum_curtailment_mw + return self._feasibility_guard.check_and_clamp( + storage_result, + curtail_result, + new_p, + new_p_th, + self._dispatch_state, ) - total_storage_curtail = self._amount_storage - self._sum_curtailment_mw - update_env_act = False - - if abs(total_storage_curtail) >= self._tol_poly: - # if there is an impact on the curtailment / storage (otherwise I cannot fix anything) - too_much = 0.0 - if sum_move > avail_up.sum(): - # I need to limit curtailment (not enough ramps up available) - too_much = dt_float(sum_move - avail_up.sum() + self._tol_poly) - self._limited_before = too_much - elif sum_move < avail_down.sum(): - # I need to limit storage unit (not enough ramps down available) - too_much = dt_float(sum_move - avail_down.sum() - self._tol_poly) - self._limited_before = too_much - elif np.abs(self._limited_before) >= self._tol_poly: - # adjust the "mess" I did before by not curtailing enough - # max_action = self.gen_pmax[gen_curtailed] * self._limit_curtailment[gen_curtailed] - update_env_act = True - too_much = min(avail_up.sum() - self._tol_poly, self._limited_before) - self._limited_before -= too_much - too_much = self._limited_before - - if abs(too_much) > self._tol_poly: - total_curtailment = ( - -self._sum_curtailment_mw / total_storage_curtail * too_much - ) - total_storage = ( - self._amount_storage / total_storage_curtail * too_much - ) # TODO !!! - update_env_act = True - # TODO "log" the total_curtailment and total_storage somewhere (in the info part of the step function) - - if np.sign(total_curtailment) != np.sign(total_storage): - # curtailment goes up, storage down, i only "limit" the one that - # has the same sign as too much - total_curtailment = ( - too_much - if np.sign(total_curtailment) == np.sign(too_much) - else 0.0 - ) - total_storage = ( - too_much if np.sign(total_storage) == np.sign(too_much) else 0.0 - ) - # NB i can directly assign all the "curtailment" to the maximum because in this case, too_much will - # necessarily be > than total_curtail (or total_storage) because the other - # one is of opposite sign - - # fix curtailment - self._aux_readjust_curtailment_after_limiting( - total_curtailment, new_p_th, new_p - ) - - # fix storage - self._aux_readjust_storage_after_limiting(total_storage) - - if update_env_act: - self._aux_update_curtail_env_act(new_p) def _aux_handle_act_inj(self, action: BaseAction): for inj_key in ["load_p", "prod_p", "load_q"]: @@ -3173,7 +2619,9 @@ def _aux_apply_redisp(self, action: BaseAction, new_p: np.ndarray, new_p_th: np.ndarray, - gen_curtailed: np.ndarray, + storage_result: StorageResult, + curtail_result: CurtailmentResult, + detach_result: DetachmentResult, except_: List[Exception], powerline_status): cls = type(self) @@ -3216,13 +2664,27 @@ def _aux_apply_redisp(self, self.redispatching_unit_commitment_availble and self._parameters.LIMIT_INFEASIBLE_CURTAILMENT_STORAGE_ACTION ): - # limit the curtailment / storage in case of infeasible redispatching - self._aux_limit_curtail_storage_if_needed(new_p, new_p_th, gen_curtailed) + self._feasibility_guard.check_and_clamp( + storage_result, + curtail_result, + new_p, + new_p_th, + self._dispatch_state, + ) self._storage_power_prev[:] = self._storage_power # case where the action modifies load (TODO maybe make a different env for that...) self._aux_handle_act_inj(action) - valid_disp, except_tmp = self._make_redisp(already_modified_gen, new_p) + constraints = RedispatchConstraints.from_results( + storage_result, + curtail_result, + detach_result, + new_p, + self._dispatch_state, + self, + ) + except_tmp = self._redispatch_solver.solve(constraints, self._dispatch_state) + valid_disp = except_tmp is None if not valid_disp or except_tmp is not None: # game over case (divergence of the scipy routine to compute redispatching) @@ -3531,27 +2993,9 @@ def _aux_run_pf_after_state_properly_set( return detailed_info, has_error def _aux_apply_detachment(self, new_p, new_p_th): - gen_detached_user = self._backend_action.get_gen_detached() - load_detached_user = self._backend_action.get_load_detached() - - # handle gen - mw_gen_lost_this = new_p[gen_detached_user].sum() - - # handle loads - mw_load_lost_this = self._prev_load_p[load_detached_user].sum() - - # put everything together - total_power_lost = -mw_gen_lost_this + mw_load_lost_this - self._detached_elements_mw = (-total_power_lost + - self._actual_dispatch[gen_detached_user].sum() - - self._detached_elements_mw_prev) - self._detached_elements_mw_prev = -total_power_lost - - # and now modifies the vectors - new_p[gen_detached_user] = 0. - new_p_th[gen_detached_user] = 0. - self._actual_dispatch[gen_detached_user] = 0. - return new_p, new_p_th + return self._detachment_module.compute( + new_p, new_p_th, self._dispatch_state, self._backend_action + ) def _aux_step_reset_action(self): action = self._action_space({}) @@ -3784,19 +3228,22 @@ def step(self, action: BaseAction) -> Tuple[BaseObservation, self._feed_data_for_detachment(new_p_th) # should be called before _axu_apply_detachment # storage unit + storage_result = StorageResult() if cls.n_storage > 0: # limiting the storage units is done in `_aux_apply_redisp` # this only ensure the Emin / Emax and all the actions - self._compute_storage(action_storage_power) + storage_result = self._compute_storage(action_storage_power) # curtailment (does not attempt to "limit" the curtailment to make sure # it is feasible) self._gen_before_curtailment[cls.gen_renewable] = new_p[cls.gen_renewable] - gen_curtailed = self._aux_handle_curtailment_without_limit(action, new_p) + curtail_result = self._aux_handle_curtailment_without_limit(action, new_p) # TODO detachment self._aux_update_backend_action(action, action_storage_power, init_disp) - new_p, new_p_th = self._aux_apply_detachment(new_p, new_p_th) + detach_result = self._aux_apply_detachment(new_p, new_p_th) + new_p = detach_result.new_p + new_p_th = detach_result.new_p_th beg__redisp = time.perf_counter() if (cls.redispatching_unit_commitment_availble or cls.n_storage > 0): @@ -3804,7 +3251,14 @@ def step(self, action: BaseAction) -> Tuple[BaseObservation, # and it is also in this function that the limiting of the curtailment / storage actions # is perform to make the state "feasible" res_disp = self._aux_apply_redisp( - action, new_p, new_p_th, gen_curtailed, except_, powerline_status + action, + new_p, + new_p_th, + storage_result, + curtail_result, + detach_result, + except_, + powerline_status, ) action, failed_redisp, is_illegal_reco, is_done = res_disp else: @@ -4210,9 +3664,18 @@ def close(self): "_is_alert_used_in_reward", "_kwargs_attention_budget", "_limited_before", + "_storage_module", + "_curtailment_module", + "_detachment_module", + "_feasibility_guard", + "_redispatch_solver", + "_dispatch_state", ]: if hasattr(self, attr_nm): - delattr(self, attr_nm) + try: + delattr(self, attr_nm) + except AttributeError: + pass setattr(self, attr_nm, None) if self._do_not_erase_local_dir_cls: diff --git a/grid2op/Environment/dispatch/__init__.py b/grid2op/Environment/dispatch/__init__.py new file mode 100644 index 000000000..48046d437 --- /dev/null +++ b/grid2op/Environment/dispatch/__init__.py @@ -0,0 +1,29 @@ +from .baseRedispatchSolver import BaseRedispatchSolver +from .curtailmentModule import CurtailmentModule +from .defaultRedispatchSolver import DefaultRedispatchSolver +from .detachmentModule import DetachmentModule +from .dispatchTypes import ( + CurtailmentResult, + DetachmentResult, + GuardInfo, + RedispatchConstraints, + RedispatchState, + StorageResult, +) +from .feasibilityGuard import FeasibilityGuard +from .storageModule import StorageModule + +__all__ = [ + "BaseRedispatchSolver", + "CurtailmentModule", + "CurtailmentResult", + "DefaultRedispatchSolver", + "DetachmentModule", + "DetachmentResult", + "FeasibilityGuard", + "GuardInfo", + "RedispatchConstraints", + "RedispatchState", + "StorageModule", + "StorageResult", +] diff --git a/grid2op/Environment/dispatch/baseRedispatchSolver.py b/grid2op/Environment/dispatch/baseRedispatchSolver.py new file mode 100644 index 000000000..af63a69b4 --- /dev/null +++ b/grid2op/Environment/dispatch/baseRedispatchSolver.py @@ -0,0 +1,33 @@ +from __future__ import annotations + +import copy +from abc import ABC, abstractmethod +from typing import Optional + +from .dispatchTypes import RedispatchConstraints, RedispatchState + + +class BaseRedispatchSolver(ABC): + def __init__(self) -> None: + self.env = None + + def bind(self, env): + self.env = env + return self + + def copy_for_env(self, env): + new_obj = copy.copy(self) + new_obj.bind(env) + return new_obj + + @abstractmethod + def solve( + self, + constraints: RedispatchConstraints, + state: RedispatchState, + ) -> Optional[Exception]: + """Compute state.actual_dispatch in-place. Return None on success.""" + + @abstractmethod + def reset(self, state: RedispatchState) -> None: + """Reset redispatch-related state.""" diff --git a/grid2op/Environment/dispatch/curtailmentModule.py b/grid2op/Environment/dispatch/curtailmentModule.py new file mode 100644 index 000000000..8faae5f57 --- /dev/null +++ b/grid2op/Environment/dispatch/curtailmentModule.py @@ -0,0 +1,67 @@ +from __future__ import annotations + +import numpy as np + +from grid2op.dtypes import dt_float + +from .dispatchTypes import CurtailmentResult, RedispatchState + + +class CurtailmentModule: + def __init__(self, env) -> None: + self.env = env + + def reset(self, state: RedispatchState) -> None: + state.limit_curtailment[self.env.gen_renewable] = 1.0 + state.limit_curtailment_prev[self.env.gen_renewable] = 1.0 + state.gen_before_curtailment[:] = 0.0 + state.sum_curtailment_mw = dt_float(0.0) + state.sum_curtailment_mw_prev = dt_float(0.0) + state.limited_before = dt_float(0.0) + + def _update_env_action(self, new_p) -> None: + if "prod_p" in self.env._env_modification._dict_inj: + self.env._env_modification._dict_inj["prod_p"][:] = new_p + else: + self.env._env_modification._dict_inj["prod_p"] = 1.0 * new_p + self.env._env_modification._modif_inj = True + + def _update_limits(self, action, state: RedispatchState) -> None: + curtailment_act = 1.0 * action._curtail + ind_curtailed_in_act = (curtailment_act != -1.0) & self.env.gen_renewable + state.limit_curtailment_prev[:] = state.limit_curtailment + state.limit_curtailment[ind_curtailed_in_act] = curtailment_act[ + ind_curtailed_in_act + ] + + def _apply_limits(self, new_p, curtailment_vect): + gen_curtailed = np.abs(curtailment_vect - 1.0) >= 1e-7 + max_action = self.env.gen_pmax[gen_curtailed] * curtailment_vect[gen_curtailed] + new_p[gen_curtailed] = np.minimum(max_action, new_p[gen_curtailed]) + return gen_curtailed + + def compute(self, action, new_p, state: RedispatchState) -> CurtailmentResult: + if self.env.redispatching_unit_commitment_availble and ( + action._modif_curtailment + or (np.abs(state.limit_curtailment - 1.0) >= 1e-7).any() + ): + self._update_limits(action, state) + gen_curtailed = self._apply_limits(new_p, state.limit_curtailment) + tmp_sum_curtailment_mw = dt_float( + new_p[gen_curtailed].sum() + - state.gen_before_curtailment[gen_curtailed].sum() + ) + state.sum_curtailment_mw = ( + tmp_sum_curtailment_mw - state.sum_curtailment_mw_prev + ) + state.sum_curtailment_mw_prev = tmp_sum_curtailment_mw + self._update_env_action(new_p) + else: + state.sum_curtailment_mw = -state.sum_curtailment_mw_prev + state.sum_curtailment_mw_prev = dt_float(0.0) + gen_curtailed = np.abs(state.limit_curtailment - 1.0) >= 1e-7 + + return CurtailmentResult( + sum_curtailment_mw=state.sum_curtailment_mw, + gen_curtailed=gen_curtailed, + ) diff --git a/grid2op/Environment/dispatch/defaultRedispatchSolver.py b/grid2op/Environment/dispatch/defaultRedispatchSolver.py new file mode 100644 index 000000000..77c333ee5 --- /dev/null +++ b/grid2op/Environment/dispatch/defaultRedispatchSolver.py @@ -0,0 +1,423 @@ +from __future__ import annotations + +from typing import List, Optional + +import numpy as np +from scipy.optimize import LinearConstraint, minimize + +from grid2op.Action import BaseAction +from grid2op.Exceptions import ( + GeneratorTurnedOffTooSoon, + GeneratorTurnedOnTooSoon, + IllegalRedispatching, + ImpossibleRedispatching, +) + +from .baseRedispatchSolver import BaseRedispatchSolver +from .dispatchTypes import RedispatchConstraints, RedispatchState + + +class DefaultRedispatchSolver(BaseRedispatchSolver): + def reset(self, state: RedispatchState) -> None: + state.target_dispatch[:] = 0.0 + state.already_modified_gen[:] = False + state.actual_dispatch[:] = 0.0 + state.gen_uptime[:] = 0 + state.gen_downtime[:] = 0 + state.gen_activeprod_t[:] = 0.0 + state.gen_activeprod_t_redisp[:] = 0.0 + + def _update_target_dispatch(self, action: BaseAction, state: RedispatchState): + if not action._modif_redispatch: + return state.already_modified_gen + + redisp_act_orig = action._redispatch + is_redisped = np.abs(redisp_act_orig) > 1e-7 + state.target_dispatch[state.already_modified_gen] += redisp_act_orig[ + state.already_modified_gen + ] + first_modified = (~state.already_modified_gen) & is_redisped + state.target_dispatch[first_modified] = ( + state.actual_dispatch[first_modified] + redisp_act_orig[first_modified] + ) + state.already_modified_gen[is_redisped] = True + return state.already_modified_gen + + def _validate_dispatch( + self, + action: BaseAction, + new_p, + already_modified_gen, + state: RedispatchState, + ): + cls = type(self.env) + except_ = None + info_: List[dict] = [] + valid = True + + if action._modif_redispatch: + redisp_act_orig = action._redispatch.copy() + else: + redisp_act_orig = None + + if ( + (redisp_act_orig is not None and (np.abs(redisp_act_orig) <= 1e-7).all()) + and (np.abs(state.target_dispatch) <= 1e-7).all() + and (np.abs(state.actual_dispatch) <= 1e-7).all() + ): + return valid, except_, info_ + + if redisp_act_orig is None: + redisp_act_orig = type(action)._build_attr("_redispatch") + + if (state.target_dispatch > cls.gen_pmax - cls.gen_pmin).any(): + cond_invalid = state.target_dispatch > cls.gen_pmax - cls.gen_pmin + except_ = IllegalRedispatching( + "You cannot ask for a dispatch higher than pmax - pmin [it would be always " + "invalid because, even if the sepoint is pmin, this dispatch would set it " + "to a number higher than pmax, which is impossible]. Invalid dispatch for " + "generator(s): {}".format((cond_invalid).nonzero()[0]) + ) + state.target_dispatch -= redisp_act_orig + return valid, except_, info_ + if (state.target_dispatch < cls.gen_pmin - cls.gen_pmax).any(): + cond_invalid = state.target_dispatch < cls.gen_pmin - cls.gen_pmax + except_ = IllegalRedispatching( + "You cannot ask for a dispatch lower than pmin - pmax [it would be always " + "invalid because, even if the sepoint is pmax, this dispatch would set it " + "to a number bellow pmin, which is impossible]. Invalid dispatch for " + "generator(s): {}".format((cond_invalid).nonzero()[0]) + ) + state.target_dispatch -= redisp_act_orig + return valid, except_, info_ + + if (redisp_act_orig[np.abs(new_p) <= 1e-7]).any() and self.env._forbid_dispatch_off: + except_ = IllegalRedispatching("Impossible to dispatch a turned off generator") + state.target_dispatch -= redisp_act_orig + return valid, except_, info_ + + if self.env._forbid_dispatch_off: + redisp_act_orig_cut = redisp_act_orig.copy() + redisp_act_orig_cut[np.abs(new_p) <= 1e-7] = 0.0 + if (redisp_act_orig_cut != redisp_act_orig).any(): + info_.append( + { + "INFO: redispatching cut because generator will be turned_off": ( + redisp_act_orig_cut != redisp_act_orig + ).nonzero()[0] + } + ) + return valid, except_, info_ + + def _detect_infeasible_dispatch( + self, + constraints: RedispatchConstraints, + incr_in_chronics, + avail_down, + avail_up, + state: RedispatchState, + ): + except_ = None + sum_move = ( + incr_in_chronics.sum() + + constraints.amount_storage_mw + - constraints.sum_curtailment_mw + + constraints.detached_mw + ) + avail_down_sum = avail_down.sum() + avail_up_sum = avail_up.sum() + gen_setpoint = state.gen_activeprod_t_redisp[self.env.gen_redispatchable] + if sum_move > avail_up_sum: + msg = self.env.DETAILED_REDISP_ERR_MSG.format( + sum_move=sum_move, + avail_up_sum=avail_up_sum, + gen_setpoint=np.round(gen_setpoint, decimals=2), + ramp_up=self.env.gen_max_ramp_up[self.env.gen_redispatchable], + gen_pmax=self.env.gen_pmax[self.env.gen_redispatchable], + avail_up=np.round(avail_up, decimals=2), + increase="increase", + decrease="decrease", + maximum="maximum", + pmax="pmax", + max_ramp_up="max_ramp_up", + ) + except_ = ImpossibleRedispatching(msg) + elif sum_move < avail_down_sum: + msg = self.env.DETAILED_REDISP_ERR_MSG.format( + sum_move=sum_move, + avail_up_sum=avail_down_sum, + gen_setpoint=np.round(gen_setpoint, decimals=2), + ramp_up=self.env.gen_max_ramp_down[self.env.gen_redispatchable], + gen_pmax=self.env.gen_pmin[self.env.gen_redispatchable], + avail_up=np.round(avail_up, decimals=2), + increase="decrease", + decrease="increase", + maximum="minimum", + pmax="pmin", + max_ramp_up="max_ramp_down", + ) + except_ = ImpossibleRedispatching(msg) + return except_ + + def _solve(self, constraints: RedispatchConstraints, state: RedispatchState): + except_ = None + cls = type(self.env) + this_dt_float = float + new_p = constraints.new_p + if self.env.nb_time_step == 0: + state.gen_activeprod_t_redisp[:] = new_p + + gen_participating = constraints.gen_participating.copy() + incr_in_chronics = new_p - (state.gen_activeprod_t_redisp - state.actual_dispatch) + + p_min_down = cls.gen_pmin[gen_participating] - state.gen_activeprod_t_redisp[gen_participating] + avail_down = np.maximum(p_min_down, -cls.gen_max_ramp_down[gen_participating]) + p_max_up = cls.gen_pmax[gen_participating] - state.gen_activeprod_t_redisp[gen_participating] + avail_up = np.minimum(p_max_up, cls.gen_max_ramp_up[gen_participating]) + except_ = self._detect_infeasible_dispatch( + constraints, + incr_in_chronics[gen_participating], + avail_down, + avail_up, + state, + ) + if except_ is not None: + if ( + self.env._parameters.IGNORE_MIN_UP_DOWN_TIME + and self.env._parameters.ALLOW_DISPATCH_GEN_SWITCH_OFF + ): + gen_participating_tmp = self.env.gen_redispatchable.copy() + if cls.detachment_is_allowed: + gen_participating_tmp[constraints.gen_detached] = False + p_min_down_tmp = ( + cls.gen_pmin[gen_participating_tmp] + - state.gen_activeprod_t_redisp[gen_participating_tmp] + ) + avail_down_tmp = np.maximum( + p_min_down_tmp, -cls.gen_max_ramp_down[gen_participating_tmp] + ) + p_max_up_tmp = ( + cls.gen_pmax[gen_participating_tmp] + - state.gen_activeprod_t_redisp[gen_participating_tmp] + ) + avail_up_tmp = np.minimum( + p_max_up_tmp, cls.gen_max_ramp_up[gen_participating_tmp] + ) + except_tmp = self._detect_infeasible_dispatch( + constraints, + incr_in_chronics[gen_participating_tmp], + avail_down_tmp, + avail_up_tmp, + state, + ) + if except_tmp is None: + gen_participating = gen_participating_tmp + except_ = None + else: + return except_tmp + else: + return except_ + + target_vals = state.target_dispatch[gen_participating] - state.actual_dispatch[gen_participating] + already_modified_gen_me = state.already_modified_gen[gen_participating] + target_vals_me = target_vals[already_modified_gen_me] + nb_dispatchable = gen_participating.sum() + tmp_zeros = np.zeros((1, nb_dispatchable), dtype=this_dt_float) + coeffs = 1.0 / (self.env.gen_max_ramp_up + self.env.gen_max_ramp_down + self.env._epsilon_poly) + weights = np.ones(nb_dispatchable) * coeffs[gen_participating] + weights /= weights.sum() + + if target_vals_me.shape[0] == 0: + already_modified_gen_me[:] = True + target_vals_me = target_vals[already_modified_gen_me] + + scale_x = max(np.max(np.abs(state.actual_dispatch)), 1.0) + scale_x = this_dt_float(scale_x) + target_vals_me_optim = 1.0 * (target_vals_me / scale_x) + target_vals_me_optim = target_vals_me_optim.astype(this_dt_float) + + scale_objective = max(0.5 * np.abs(target_vals_me_optim).sum() ** 2, 1.0) + scale_objective = np.round(scale_objective, decimals=4) + scale_objective = this_dt_float(scale_objective) + + mat_sum_0_no_turn_on = np.ones((1, nb_dispatchable), dtype=this_dt_float) + const_sum_0_no_turn_on = ( + np.zeros(1, dtype=this_dt_float) + + constraints.amount_storage_mw + - constraints.sum_curtailment_mw + + constraints.detached_mw + ) + + new_p_th = new_p[gen_participating] + state.actual_dispatch[gen_participating] + p_min_const = self.env.gen_pmin[gen_participating] - new_p_th + ramp_down_const = -self.env.gen_max_ramp_down[gen_participating] - incr_in_chronics[gen_participating] + min_disp = np.maximum(p_min_const, ramp_down_const).astype(this_dt_float) + + p_max_const = self.env.gen_pmax[gen_participating] - new_p_th + ramp_up_const = self.env.gen_max_ramp_up[gen_participating] - incr_in_chronics[gen_participating] + max_disp = np.minimum(p_max_const, ramp_up_const).astype(this_dt_float) + + added = 0.5 * self.env._epsilon_poly + equality_const = LinearConstraint( + mat_sum_0_no_turn_on, + const_sum_0_no_turn_on / scale_x, + const_sum_0_no_turn_on / scale_x, + ) + mat_pmin_max_ramps = np.eye(nb_dispatchable) + ineq_const = LinearConstraint( + mat_pmin_max_ramps, + (min_disp - added) / scale_x, + (max_disp + added) / scale_x, + ) + + x0 = np.zeros(gen_participating.sum(), dtype=this_dt_float) + if (np.abs(state.target_dispatch) >= 1e-7).any() or state.already_modified_gen.any(): + gen_for_x0 = np.abs(state.target_dispatch[gen_participating]) >= 1e-7 + gen_for_x0 |= state.already_modified_gen[gen_participating] + x0[gen_for_x0] = ( + state.target_dispatch[gen_participating][gen_for_x0] + - state.actual_dispatch[gen_participating][gen_for_x0] + ) / scale_x + can_adjust = np.abs(x0) <= 1e-7 + if can_adjust.any(): + init_sum = x0.sum() + denom_adjust = (1.0 / weights[can_adjust]).sum() + if denom_adjust <= 1e-2: + denom_adjust = 1.0 + x0[can_adjust] = -init_sum / (weights[can_adjust] * denom_adjust) + else: + x0 -= state.actual_dispatch[gen_participating] / scale_x + + def target(actual_dispatchable): + quad_ = ( + actual_dispatchable[already_modified_gen_me] - target_vals_me_optim + ) ** 2 + coeffs_quads = weights[already_modified_gen_me] * quad_ + coeffs_quads_const = coeffs_quads.sum() + coeffs_quads_const /= scale_objective + return coeffs_quads_const + + def jac(actual_dispatchable): + res_jac = 1.0 * tmp_zeros + res_jac[0, already_modified_gen_me] = ( + 2.0 + * weights[already_modified_gen_me] + * (actual_dispatchable[already_modified_gen_me] - target_vals_me_optim) + ) + res_jac /= scale_objective + return res_jac.reshape(-1) + + res = minimize( + target, + x0, + method="SLSQP", + constraints=[equality_const, ineq_const], + options={ + "eps": max(this_dt_float(self.env._epsilon_poly / scale_x), 1e-6), + "ftol": max(this_dt_float(self.env._epsilon_poly / scale_x), 1e-6), + "disp": False, + }, + jac=jac, + ) + if res.success: + state.actual_dispatch[gen_participating] += res.x * scale_x + else: + mat_const = np.concatenate((mat_sum_0_no_turn_on, mat_pmin_max_ramps)) + downs = np.concatenate( + (const_sum_0_no_turn_on / scale_x, (min_disp - added) / scale_x) + ) + ups = np.concatenate( + (const_sum_0_no_turn_on / scale_x, (max_disp + added) / scale_x) + ) + vals = np.matmul(mat_const, res.x) + ok_down = np.all(vals - downs >= -self.env._tol_poly) + ok_up = np.all(vals - ups <= self.env._tol_poly) + if ok_up and ok_down: + state.actual_dispatch[gen_participating] += res.x * scale_x + else: + error_dispatch = ( + "Redispatching automaton terminated with error (no more information available " + 'at this point):\n"{}"'.format(res.message) + ) + except_ = ImpossibleRedispatching(error_dispatch) + return except_ + + def solve( + self, + constraints: RedispatchConstraints, + state: RedispatchState, + ) -> Optional[Exception]: + if not self.env._parameters.ENV_DOES_REDISPATCHING: + state.actual_dispatch[:] = state.target_dispatch.copy() + return None + mismatch = np.abs(state.actual_dispatch - state.target_dispatch) + if ( + np.abs((state.actual_dispatch).sum()) >= self.env._tol_poly + or np.max(mismatch) >= self.env._tol_poly + or np.abs(state.amount_storage) >= self.env._tol_poly + or np.abs(state.sum_curtailment_mw) >= self.env._tol_poly + or np.abs(state.detached_elements_mw) >= self.env._tol_poly + ): + return self._solve(constraints, state) + return None + + def _check_updown_times(self, gen_up_before, redisp_act, state: RedispatchState): + except_ = None + cls = type(self.env) + gen_up_after = state.gen_activeprod_t.copy() + if "prod_p" in self.env._env_modification._dict_inj: + tmp = self.env._env_modification._dict_inj["prod_p"] + indx_ok = np.isfinite(tmp) + gen_up_after[indx_ok] = self.env._env_modification._dict_inj["prod_p"][indx_ok] + gen_up_after += redisp_act + gen_up_after = np.abs(gen_up_after) > 1e-7 + + gen_disconnected_this = gen_up_before & (~gen_up_after) + gen_connected_this_timestep = (~gen_up_before) & gen_up_after & ~self.env._gens_detached + gen_still_connected = gen_up_before & gen_up_after + gen_still_disconnected = ((~gen_up_before) & (~gen_up_after)) | self.env._gens_detached + if ( + not self.env._ignore_min_up_down_times + and ( + state.gen_downtime[gen_connected_this_timestep] + < cls.gen_min_downtime[gen_connected_this_timestep] + ).any() + ): + id_gen = ( + state.gen_downtime[gen_connected_this_timestep] + < cls.gen_min_downtime[gen_connected_this_timestep] + ) + id_gen = (id_gen).nonzero()[0] + id_gen = (gen_connected_this_timestep[id_gen]).nonzero()[0] + except_ = GeneratorTurnedOnTooSoon( + "Some generator has been connected too early ({})".format(id_gen) + ) + return except_ + else: + state.gen_downtime[gen_connected_this_timestep] = -1 + state.gen_uptime[gen_connected_this_timestep] = 0 + + if ( + not self.env._ignore_min_up_down_times + and ( + state.gen_uptime[gen_disconnected_this] + < cls.gen_min_uptime[gen_disconnected_this] + ).any() + ): + id_gen = ( + state.gen_uptime[gen_disconnected_this] + < cls.gen_min_uptime[gen_disconnected_this] + ) + id_gen = (id_gen).nonzero()[0] + id_gen = (gen_disconnected_this[id_gen]).nonzero()[0] + except_ = GeneratorTurnedOffTooSoon( + "Some generator has been disconnected too early ({})".format(id_gen) + ) + return except_ + else: + state.gen_downtime[gen_disconnected_this] = 0 + state.gen_uptime[gen_disconnected_this] = -1 + + state.gen_uptime[gen_still_connected] += 1 + state.gen_downtime[gen_still_disconnected] += 1 + return except_ diff --git a/grid2op/Environment/dispatch/detachmentModule.py b/grid2op/Environment/dispatch/detachmentModule.py new file mode 100644 index 000000000..01f97e241 --- /dev/null +++ b/grid2op/Environment/dispatch/detachmentModule.py @@ -0,0 +1,47 @@ +from __future__ import annotations + +from grid2op.dtypes import dt_float + +from .dispatchTypes import DetachmentResult, RedispatchState + + +class DetachmentModule: + def __init__(self, env) -> None: + self.env = env + + def feed_data(self, new_p_th, state: RedispatchState) -> None: + self.env._prev_gen_p[:] = new_p_th + self.env._aux_retrieve_modif_act(self.env._prev_load_p, self.env._env_modification, "load_p") + self.env._aux_retrieve_modif_act(self.env._prev_load_q, self.env._env_modification, "load_q") + + def compute( + self, + new_p, + new_p_th, + state: RedispatchState, + backend_action=None, + ) -> DetachmentResult: + if backend_action is None: + backend_action = self.env._backend_action + gen_detached_user = backend_action.get_gen_detached() + load_detached_user = backend_action.get_load_detached() + + mw_gen_lost_this = new_p[gen_detached_user].sum() + mw_load_lost_this = self.env._prev_load_p[load_detached_user].sum() + total_power_lost = -mw_gen_lost_this + mw_load_lost_this + state.detached_elements_mw = dt_float( + -total_power_lost + + state.actual_dispatch[gen_detached_user].sum() + - state.detached_elements_mw_prev + ) + state.detached_elements_mw_prev = dt_float(-total_power_lost) + + new_p[gen_detached_user] = 0.0 + new_p_th[gen_detached_user] = 0.0 + state.actual_dispatch[gen_detached_user] = 0.0 + return DetachmentResult( + detached_mw=state.detached_elements_mw, + gen_detached=gen_detached_user, + new_p=new_p, + new_p_th=new_p_th, + ) diff --git a/grid2op/Environment/dispatch/dispatchTypes.py b/grid2op/Environment/dispatch/dispatchTypes.py new file mode 100644 index 000000000..feb436385 --- /dev/null +++ b/grid2op/Environment/dispatch/dispatchTypes.py @@ -0,0 +1,137 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Optional, TYPE_CHECKING + +import numpy as np + +from grid2op.dtypes import dt_bool, dt_float, dt_int + +if TYPE_CHECKING: + from grid2op.Environment.baseEnv import BaseEnv + + +@dataclass +class StorageResult: + amount_storage_mw: np.float64 = dt_float(0.0) + storage_power: Optional[np.ndarray] = None + + +@dataclass +class CurtailmentResult: + sum_curtailment_mw: np.float64 = dt_float(0.0) + gen_curtailed: Optional[np.ndarray] = None + + +@dataclass +class DetachmentResult: + detached_mw: np.float64 = dt_float(0.0) + gen_detached: Optional[np.ndarray] = None + new_p: Optional[np.ndarray] = None + new_p_th: Optional[np.ndarray] = None + + +@dataclass +class GuardInfo: + total_curtailment: np.float64 = dt_float(0.0) + total_storage: np.float64 = dt_float(0.0) + updated_env_action: bool = False + + +@dataclass +class RedispatchState: + target_dispatch: Optional[np.ndarray] = None + already_modified_gen: Optional[np.ndarray] = None + actual_dispatch: Optional[np.ndarray] = None + gen_uptime: Optional[np.ndarray] = None + gen_downtime: Optional[np.ndarray] = None + gen_activeprod_t: Optional[np.ndarray] = None + gen_activeprod_t_redisp: Optional[np.ndarray] = None + storage_current_charge: Optional[np.ndarray] = None + storage_previous_charge: Optional[np.ndarray] = None + action_storage: Optional[np.ndarray] = None + amount_storage: np.float64 = dt_float(0.0) + amount_storage_prev: np.float64 = dt_float(0.0) + storage_power: Optional[np.ndarray] = None + storage_power_prev: Optional[np.ndarray] = None + limit_curtailment: Optional[np.ndarray] = None + limit_curtailment_prev: Optional[np.ndarray] = None + gen_before_curtailment: Optional[np.ndarray] = None + sum_curtailment_mw: np.float64 = dt_float(0.0) + sum_curtailment_mw_prev: np.float64 = dt_float(0.0) + detached_elements_mw: np.float64 = dt_float(0.0) + detached_elements_mw_prev: np.float64 = dt_float(0.0) + limited_before: np.float64 = dt_float(0.0) + + @classmethod + def allocate(cls, n_gen: int, n_storage: int) -> "RedispatchState": + return cls( + target_dispatch=np.zeros(n_gen, dtype=dt_float), + already_modified_gen=np.zeros(n_gen, dtype=dt_bool), + actual_dispatch=np.zeros(n_gen, dtype=dt_float), + gen_uptime=np.zeros(n_gen, dtype=dt_int), + gen_downtime=np.zeros(n_gen, dtype=dt_int), + gen_activeprod_t=np.zeros(n_gen, dtype=dt_float), + gen_activeprod_t_redisp=np.zeros(n_gen, dtype=dt_float), + storage_current_charge=np.zeros(n_storage, dtype=dt_float), + storage_previous_charge=np.zeros(n_storage, dtype=dt_float), + action_storage=np.zeros(n_storage, dtype=dt_float), + storage_power=np.zeros(n_storage, dtype=dt_float), + storage_power_prev=np.zeros(n_storage, dtype=dt_float), + limit_curtailment=np.ones(n_gen, dtype=dt_float), + limit_curtailment_prev=np.ones(n_gen, dtype=dt_float), + gen_before_curtailment=np.zeros(n_gen, dtype=dt_float), + ) + + +@dataclass(frozen=True) +class RedispatchConstraints: + new_p: np.ndarray + gen_activeprod_t: np.ndarray + target_dispatch: np.ndarray + gen_participating: np.ndarray + amount_storage_mw: np.float64 + sum_curtailment_mw: np.float64 + detached_mw: np.float64 + pmin: np.ndarray + pmax: np.ndarray + ramp_up: np.ndarray + ramp_down: np.ndarray + redispatchable_mask: np.ndarray + gen_detached: np.ndarray + + @classmethod + def from_results( + cls, + storage_result: Optional[StorageResult], + curtail_result: Optional[CurtailmentResult], + detach_result: Optional[DetachmentResult], + new_p: np.ndarray, + state: RedispatchState, + env: "BaseEnv", + ) -> "RedispatchConstraints": + cls_env = type(env) + gen_detached = env._backend_action.get_gen_detached() + gen_participating = ( + (new_p > 0.0) + | (np.abs(state.actual_dispatch) >= 1e-7) + | (state.target_dispatch != state.actual_dispatch) + ) + gen_participating[~cls_env.gen_redispatchable] = False + if cls_env.detachment_is_allowed: + gen_participating[gen_detached] = False + return cls( + new_p=new_p, + gen_activeprod_t=state.gen_activeprod_t, + target_dispatch=state.target_dispatch, + gen_participating=gen_participating, + amount_storage_mw=dt_float(state.amount_storage), + sum_curtailment_mw=dt_float(state.sum_curtailment_mw), + detached_mw=dt_float(state.detached_elements_mw), + pmin=cls_env.gen_pmin, + pmax=cls_env.gen_pmax, + ramp_up=cls_env.gen_max_ramp_up, + ramp_down=cls_env.gen_max_ramp_down, + redispatchable_mask=cls_env.gen_redispatchable, + gen_detached=gen_detached, + ) diff --git a/grid2op/Environment/dispatch/feasibilityGuard.py b/grid2op/Environment/dispatch/feasibilityGuard.py new file mode 100644 index 000000000..38c7d944a --- /dev/null +++ b/grid2op/Environment/dispatch/feasibilityGuard.py @@ -0,0 +1,150 @@ +from __future__ import annotations + +import numpy as np + +from grid2op.dtypes import dt_float + +from .dispatchTypes import CurtailmentResult, GuardInfo, RedispatchState, StorageResult + + +class FeasibilityGuard: + def __init__(self, env) -> None: + self.env = env + + def _compute_ramp_budget(self, new_p, state: RedispatchState): + th_max = np.minimum( + state.gen_activeprod_t_redisp[self.env.gen_redispatchable] + + self.env.gen_max_ramp_up[self.env.gen_redispatchable], + self.env.gen_pmax[self.env.gen_redispatchable], + ) + th_min = np.maximum( + state.gen_activeprod_t_redisp[self.env.gen_redispatchable] + - self.env.gen_max_ramp_down[self.env.gen_redispatchable], + self.env.gen_pmin[self.env.gen_redispatchable], + ) + + max_total_up = (th_max - new_p[self.env.gen_redispatchable]).sum() + max_total_down = (th_min - new_p[self.env.gen_redispatchable]).sum() + return max_total_down, max_total_up + + def _readjust_curtailment( + self, + total_curtailment, + new_p_th, + new_p, + state: RedispatchState, + ) -> None: + state.sum_curtailment_mw += total_curtailment + state.sum_curtailment_mw_prev += total_curtailment + if total_curtailment > self.env._tol_poly: + curtailed = new_p_th - new_p + else: + new_p_with_previous_curtailment = 1.0 * new_p_th + self.env._curtailment_module._apply_limits( + new_p_with_previous_curtailment, state.limit_curtailment_prev + ) + curtailed = new_p_th - new_p_with_previous_curtailment + + curt_sum = curtailed.sum() + if abs(curt_sum) > self.env._tol_poly: + curtailed[~self.env.gen_renewable] = 0.0 + curtailed *= total_curtailment / curt_sum + new_p[self.env.gen_renewable] += curtailed[self.env.gen_renewable] + + def _readjust_storage(self, total_storage, state: RedispatchState) -> None: + new_act_storage = 1.0 * state.storage_power + sum_this_step = new_act_storage.sum() + if abs(total_storage) < abs(sum_this_step): + modif_storage = new_act_storage * total_storage / sum_this_step + else: + new_act_storage = 1.0 * state.storage_power_prev + sum_this_step = new_act_storage.sum() + if abs(sum_this_step) > 1e-1: + modif_storage = new_act_storage * total_storage / sum_this_step + else: + modif_storage = new_act_storage + + coeff_p_to_E = self.env.delta_time_seconds / 3600.0 + state.storage_power -= modif_storage + + is_discharging = state.storage_power < 0.0 + is_charging = state.storage_power > 0.0 + modif_storage[is_discharging] /= type(self.env).storage_discharging_efficiency[ + is_discharging + ] + modif_storage[is_charging] *= type(self.env).storage_charging_efficiency[ + is_charging + ] + + state.storage_current_charge -= coeff_p_to_E * modif_storage + state.amount_storage -= total_storage + state.amount_storage_prev -= total_storage + + def check_and_clamp( + self, + storage_result: StorageResult, + curtail_result: CurtailmentResult, + new_p, + new_p_th, + state: RedispatchState, + ) -> GuardInfo: + gen_redisp = self.env.gen_redispatchable + normal_increase = new_p - ( + state.gen_activeprod_t_redisp - state.actual_dispatch + ) + normal_increase = normal_increase[gen_redisp] + p_min_down = self.env.gen_pmin[gen_redisp] - state.gen_activeprod_t_redisp[gen_redisp] + avail_down = np.maximum(p_min_down, -self.env.gen_max_ramp_down[gen_redisp]) + p_max_up = self.env.gen_pmax[gen_redisp] - state.gen_activeprod_t_redisp[gen_redisp] + avail_up = np.minimum(p_max_up, self.env.gen_max_ramp_up[gen_redisp]) + + sum_move = normal_increase.sum() + state.amount_storage - state.sum_curtailment_mw + total_storage_curtail = state.amount_storage - state.sum_curtailment_mw + update_env_act = False + total_curtailment = dt_float(0.0) + total_storage = dt_float(0.0) + + if abs(total_storage_curtail) >= self.env._tol_poly: + too_much = 0.0 + if sum_move > avail_up.sum(): + too_much = dt_float(sum_move - avail_up.sum() + self.env._tol_poly) + state.limited_before = too_much + elif sum_move < avail_down.sum(): + too_much = dt_float(sum_move - avail_down.sum() - self.env._tol_poly) + state.limited_before = too_much + elif np.abs(state.limited_before) >= self.env._tol_poly: + update_env_act = True + too_much = min(avail_up.sum() - self.env._tol_poly, state.limited_before) + state.limited_before -= too_much + too_much = state.limited_before + + if abs(too_much) > self.env._tol_poly: + total_curtailment = dt_float( + -state.sum_curtailment_mw / total_storage_curtail * too_much + ) + total_storage = dt_float( + state.amount_storage / total_storage_curtail * too_much + ) + update_env_act = True + + if np.sign(total_curtailment) != np.sign(total_storage): + total_curtailment = ( + too_much + if np.sign(total_curtailment) == np.sign(too_much) + else 0.0 + ) + total_storage = ( + too_much if np.sign(total_storage) == np.sign(too_much) else 0.0 + ) + + self._readjust_curtailment(total_curtailment, new_p_th, new_p, state) + self._readjust_storage(total_storage, state) + + if update_env_act: + self.env._curtailment_module._update_env_action(new_p) + + return GuardInfo( + total_curtailment=dt_float(total_curtailment), + total_storage=dt_float(total_storage), + updated_env_action=update_env_act, + ) diff --git a/grid2op/Environment/dispatch/storageModule.py b/grid2op/Environment/dispatch/storageModule.py new file mode 100644 index 000000000..81d59f585 --- /dev/null +++ b/grid2op/Environment/dispatch/storageModule.py @@ -0,0 +1,109 @@ +from __future__ import annotations + +import numpy as np + +from .dispatchTypes import RedispatchState, StorageResult + + +class StorageModule: + def __init__(self, env) -> None: + self.env = env + + def reset(self, state: RedispatchState) -> None: + if self.env.n_storage > 0: + tmp = self.env._parameters.INIT_STORAGE_CAPACITY * self.env.storage_Emax + if self.env._parameters.ACTIVATE_STORAGE_LOSS: + tmp += self.env.storage_loss * self.env.delta_time_seconds / 3600.0 + state.storage_previous_charge[:] = tmp + state.storage_current_charge[:] = tmp + state.storage_power[:] = 0.0 + state.storage_power_prev[:] = 0.0 + state.amount_storage = 0.0 + state.amount_storage_prev = 0.0 + + def withdraw_losses(self, state: RedispatchState) -> None: + if self.env._parameters.ACTIVATE_STORAGE_LOSS: + tmp_ = self.env.storage_loss * self.env.delta_time_seconds / 3600.0 + state.storage_current_charge -= tmp_ + state.storage_current_charge[:] = np.maximum( + state.storage_current_charge, 0.0 + ) + + def _clamp_too_high(self, delta_, indx_too_high, state: RedispatchState) -> None: + coeff_p_to_E = self.env.delta_time_seconds / 3600.0 + tmp_ = 1.0 / coeff_p_to_E * delta_ + if self.env._parameters.ACTIVATE_STORAGE_LOSS: + tmp_ /= self.env.storage_charging_efficiency[indx_too_high] + state.storage_power[indx_too_high] -= tmp_ + + def _clamp_too_low(self, delta_, indx_too_low, state: RedispatchState) -> None: + coeff_p_to_E = self.env.delta_time_seconds / 3600.0 + tmp_ = 1.0 / coeff_p_to_E * delta_ + if self.env._parameters.ACTIVATE_STORAGE_LOSS: + tmp_ *= self.env.storage_discharging_efficiency[indx_too_low] + state.storage_power[indx_too_low] -= tmp_ + + def compute(self, action_storage_power, state: RedispatchState) -> StorageResult: + state.storage_previous_charge[:] = state.storage_current_charge + storage_act = np.isfinite(action_storage_power) & ( + np.abs(action_storage_power) >= 1e-7 + ) + state.action_storage[:] = 0.0 + state.storage_power[:] = 0.0 + modif = False + coeff_p_to_E = self.env.delta_time_seconds / 3600.0 + if storage_act.any(): + modif = True + this_act_stor = action_storage_power[storage_act] + eff_ = np.ones(storage_act.sum()) + if self.env._parameters.ACTIVATE_STORAGE_LOSS: + fill_storage = this_act_stor > 0.0 + unfill_storage = this_act_stor < 0.0 + eff_[fill_storage] *= self.env.storage_charging_efficiency[storage_act][ + fill_storage + ] + eff_[unfill_storage] /= self.env.storage_discharging_efficiency[ + storage_act + ][unfill_storage] + state.storage_current_charge[storage_act] += this_act_stor * coeff_p_to_E * eff_ + state.action_storage[storage_act] += action_storage_power[storage_act] + state.storage_power[storage_act] = this_act_stor + + if modif: + indx_too_high = state.storage_current_charge > self.env.storage_Emax + if indx_too_high.any(): + delta_ = ( + state.storage_current_charge[indx_too_high] + - self.env.storage_Emax[indx_too_high] + ) + self._clamp_too_high(delta_, indx_too_high, state) + state.storage_current_charge[indx_too_high] = self.env.storage_Emax[ + indx_too_high + ] + + indx_too_low = state.storage_current_charge < self.env.storage_Emin + if indx_too_low.any(): + delta_ = ( + state.storage_current_charge[indx_too_low] + - self.env.storage_Emin[indx_too_low] + ) + self._clamp_too_low(delta_, indx_too_low, state) + state.storage_current_charge[indx_too_low] = self.env.storage_Emin[ + indx_too_low + ] + + state.storage_current_charge[:] = np.maximum( + state.storage_current_charge, self.env.storage_Emin + ) + state.amount_storage = state.storage_power.sum() + else: + state.amount_storage = 0.0 + + tmp = state.amount_storage + state.amount_storage -= state.amount_storage_prev + state.amount_storage_prev = tmp + self.withdraw_losses(state) + return StorageResult( + amount_storage_mw=state.amount_storage, + storage_power=state.storage_power, + ) diff --git a/grid2op/Environment/environment.py b/grid2op/Environment/environment.py index 2e0a7c85c..b3389b7a9 100644 --- a/grid2op/Environment/environment.py +++ b/grid2op/Environment/environment.py @@ -130,6 +130,7 @@ def __init__( _allow_loaded_backend=False, _local_dir_cls=None, # only set at the first call to `make(...)` after should be false _overload_name_multimix=None, + redispatch_solver=None, ): if other_rewards is None: other_rewards = {} @@ -179,6 +180,7 @@ def __init__( _is_test=_is_test, # is this created with "test=True" # TODO not implemented !! _local_dir_cls=_local_dir_cls, _read_from_local_dir=_read_from_local_dir, + redispatch_solver=redispatch_solver, ) if name == "unknown": From 20c8cdca0269ac7b54a9aa5b504d5d1438dc3c0b Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Fri, 15 May 2026 11:31:47 +0200 Subject: [PATCH 7/8] redispatch in its own module, remove python 3.8 from github ci Signed-off-by: DONNOT Benjamin --- .github/workflows/main.yml | 10 +++++----- CHANGELOG.rst | 4 +++- grid2op/Chronics/fromNPY.py | 2 +- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c9286822d..a8095a156 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -18,11 +18,11 @@ jobs: strategy: matrix: python: - - { - name: cp38, - abi: cp38, - version: '3.8', - } + # - { + # name: cp38, + # abi: cp38, + # version: '3.8', + # } - { name: cp39, abi: cp39, diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 83d15bcbe..c886ac6a9 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -104,7 +104,9 @@ Native multi agents support: -------------------- - [FIXED] license issues in the documentation - [IMPROVED] tests splitting to reduce the duration on circle ci - +- [IMPROVED] handling of redispatching as a separate module now + (grid2op/Environment/dispatch) + [1.12.4] - 2026-04-28 ---------------------- - [BREAKING] the behaviour of grid2op environment when ENV_DOES_REDISPATCHING diff --git a/grid2op/Chronics/fromNPY.py b/grid2op/Chronics/fromNPY.py index 3c7f03a88..efdbc8400 100644 --- a/grid2op/Chronics/fromNPY.py +++ b/grid2op/Chronics/fromNPY.py @@ -417,7 +417,7 @@ def check_validity( if self._prod_p.shape[0] != self._forecasts._prod_p.shape[0]: raise ChronicsError("self._prod_p.shape[0] != self._forecasts._prod_p.shape[0]") if self._prod_v is not None and self._forecasts._prod_v is not None: - if self._prod_v.shape[0] == self._forecasts._prod_v.shape[0]: + if self._prod_v.shape[0] != self._forecasts._prod_v.shape[0]: raise ChronicsError("self._prod_v.shape[0] != self._forecasts._prod_v.shape[0]") self._forecasts.check_validity(backend=backend) From 07d4334d869e290063deedeb418b77deac128d04 Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Fri, 15 May 2026 13:10:03 +0200 Subject: [PATCH 8/8] adding license headers in some files Signed-off-by: DONNOT Benjamin --- CHANGELOG.rst | 3 ++- grid2op/Environment/dispatch/__init__.py | 8 ++++++++ grid2op/Environment/dispatch/baseRedispatchSolver.py | 8 ++++++++ grid2op/Environment/dispatch/curtailmentModule.py | 8 ++++++++ grid2op/Environment/dispatch/defaultRedispatchSolver.py | 8 ++++++++ grid2op/Environment/dispatch/detachmentModule.py | 8 ++++++++ grid2op/Environment/dispatch/dispatchTypes.py | 8 ++++++++ grid2op/Environment/dispatch/feasibilityGuard.py | 8 ++++++++ grid2op/Environment/dispatch/storageModule.py | 8 ++++++++ 9 files changed, 66 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index c886ac6a9..22937bce9 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -106,7 +106,8 @@ Native multi agents support: - [IMPROVED] tests splitting to reduce the duration on circle ci - [IMPROVED] handling of redispatching as a separate module now (grid2op/Environment/dispatch) - +- [IMPROVED] remove the use of "assert" block in the main codebase + [1.12.4] - 2026-04-28 ---------------------- - [BREAKING] the behaviour of grid2op environment when ENV_DOES_REDISPATCHING diff --git a/grid2op/Environment/dispatch/__init__.py b/grid2op/Environment/dispatch/__init__.py index 48046d437..479828d83 100644 --- a/grid2op/Environment/dispatch/__init__.py +++ b/grid2op/Environment/dispatch/__init__.py @@ -1,3 +1,11 @@ +# Copyright (c) 2026, RTE (https://www.rte-france.com) +# See AUTHORS.txt +# This Source Code Form is subject to the terms of the Mozilla Public License, version 2.0. +# If a copy of the Mozilla Public License, version 2.0 was not distributed with this file, +# you can obtain one at http://mozilla.org/MPL/2.0/. +# SPDX-License-Identifier: MPL-2.0 +# This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. + from .baseRedispatchSolver import BaseRedispatchSolver from .curtailmentModule import CurtailmentModule from .defaultRedispatchSolver import DefaultRedispatchSolver diff --git a/grid2op/Environment/dispatch/baseRedispatchSolver.py b/grid2op/Environment/dispatch/baseRedispatchSolver.py index af63a69b4..1a0ebc348 100644 --- a/grid2op/Environment/dispatch/baseRedispatchSolver.py +++ b/grid2op/Environment/dispatch/baseRedispatchSolver.py @@ -1,3 +1,11 @@ +# Copyright (c) 2026, RTE (https://www.rte-france.com) +# See AUTHORS.txt +# This Source Code Form is subject to the terms of the Mozilla Public License, version 2.0. +# If a copy of the Mozilla Public License, version 2.0 was not distributed with this file, +# you can obtain one at http://mozilla.org/MPL/2.0/. +# SPDX-License-Identifier: MPL-2.0 +# This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. + from __future__ import annotations import copy diff --git a/grid2op/Environment/dispatch/curtailmentModule.py b/grid2op/Environment/dispatch/curtailmentModule.py index 8faae5f57..642043eb4 100644 --- a/grid2op/Environment/dispatch/curtailmentModule.py +++ b/grid2op/Environment/dispatch/curtailmentModule.py @@ -1,3 +1,11 @@ +# Copyright (c) 2026, RTE (https://www.rte-france.com) +# See AUTHORS.txt +# This Source Code Form is subject to the terms of the Mozilla Public License, version 2.0. +# If a copy of the Mozilla Public License, version 2.0 was not distributed with this file, +# you can obtain one at http://mozilla.org/MPL/2.0/. +# SPDX-License-Identifier: MPL-2.0 +# This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. + from __future__ import annotations import numpy as np diff --git a/grid2op/Environment/dispatch/defaultRedispatchSolver.py b/grid2op/Environment/dispatch/defaultRedispatchSolver.py index 77c333ee5..260a828de 100644 --- a/grid2op/Environment/dispatch/defaultRedispatchSolver.py +++ b/grid2op/Environment/dispatch/defaultRedispatchSolver.py @@ -1,3 +1,11 @@ +# Copyright (c) 2026, RTE (https://www.rte-france.com) +# See AUTHORS.txt +# This Source Code Form is subject to the terms of the Mozilla Public License, version 2.0. +# If a copy of the Mozilla Public License, version 2.0 was not distributed with this file, +# you can obtain one at http://mozilla.org/MPL/2.0/. +# SPDX-License-Identifier: MPL-2.0 +# This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. + from __future__ import annotations from typing import List, Optional diff --git a/grid2op/Environment/dispatch/detachmentModule.py b/grid2op/Environment/dispatch/detachmentModule.py index 01f97e241..70e88c2fa 100644 --- a/grid2op/Environment/dispatch/detachmentModule.py +++ b/grid2op/Environment/dispatch/detachmentModule.py @@ -1,3 +1,11 @@ +# Copyright (c) 2026, RTE (https://www.rte-france.com) +# See AUTHORS.txt +# This Source Code Form is subject to the terms of the Mozilla Public License, version 2.0. +# If a copy of the Mozilla Public License, version 2.0 was not distributed with this file, +# you can obtain one at http://mozilla.org/MPL/2.0/. +# SPDX-License-Identifier: MPL-2.0 +# This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. + from __future__ import annotations from grid2op.dtypes import dt_float diff --git a/grid2op/Environment/dispatch/dispatchTypes.py b/grid2op/Environment/dispatch/dispatchTypes.py index feb436385..fcff1e6ac 100644 --- a/grid2op/Environment/dispatch/dispatchTypes.py +++ b/grid2op/Environment/dispatch/dispatchTypes.py @@ -1,3 +1,11 @@ +# Copyright (c) 2026, RTE (https://www.rte-france.com) +# See AUTHORS.txt +# This Source Code Form is subject to the terms of the Mozilla Public License, version 2.0. +# If a copy of the Mozilla Public License, version 2.0 was not distributed with this file, +# you can obtain one at http://mozilla.org/MPL/2.0/. +# SPDX-License-Identifier: MPL-2.0 +# This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. + from __future__ import annotations from dataclasses import dataclass diff --git a/grid2op/Environment/dispatch/feasibilityGuard.py b/grid2op/Environment/dispatch/feasibilityGuard.py index 38c7d944a..db615f581 100644 --- a/grid2op/Environment/dispatch/feasibilityGuard.py +++ b/grid2op/Environment/dispatch/feasibilityGuard.py @@ -1,3 +1,11 @@ +# Copyright (c) 2026, RTE (https://www.rte-france.com) +# See AUTHORS.txt +# This Source Code Form is subject to the terms of the Mozilla Public License, version 2.0. +# If a copy of the Mozilla Public License, version 2.0 was not distributed with this file, +# you can obtain one at http://mozilla.org/MPL/2.0/. +# SPDX-License-Identifier: MPL-2.0 +# This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. + from __future__ import annotations import numpy as np diff --git a/grid2op/Environment/dispatch/storageModule.py b/grid2op/Environment/dispatch/storageModule.py index 81d59f585..0274b93b8 100644 --- a/grid2op/Environment/dispatch/storageModule.py +++ b/grid2op/Environment/dispatch/storageModule.py @@ -1,3 +1,11 @@ +# Copyright (c) 2026, RTE (https://www.rte-france.com) +# See AUTHORS.txt +# This Source Code Form is subject to the terms of the Mozilla Public License, version 2.0. +# If a copy of the Mozilla Public License, version 2.0 was not distributed with this file, +# you can obtain one at http://mozilla.org/MPL/2.0/. +# SPDX-License-Identifier: MPL-2.0 +# This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. + from __future__ import annotations import numpy as np