diff --git a/.circleci/config.yml b/.circleci/config.yml index 01e9dab4c..7f8a504f7 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -140,7 +140,7 @@ jobs: - run: command: | source venv_test/bin/activate - python -m pip install -U "numpy>=1.20,<1.21" "pandas<2.2" "scipy<1.12" numba .[test] + python -m pip install -U "numpy>=1.20,<1.21" "pandas<2.2" "scipy<1.12" numba "pillow<10.4.0" .[test] pip freeze - run: command: | @@ -151,7 +151,7 @@ jobs: - run: command: | source venv_test/bin/activate - python -m pip install -U "numpy>=1.24,<1.25" "pandas<2.2" "scipy<1.12" numba .[test] + python -m pip install -U "numpy>=1.24,<1.25" "pandas<2.2" "scipy<1.12" numba "pillow<10.4.0" .[test] - run: command: | source venv_test/bin/activate @@ -209,7 +209,7 @@ jobs: - run: command: | source venv_test/bin/activate - python -m pip install -e . + python -m pip install -e . pip freeze - run: command: | @@ -217,6 +217,32 @@ jobs: export _GRID2OP_FORCE_TEST=1 python -m unittest grid2op/tests/test_basic_env_ls.py + test_chronix2grid: + executor: python310 # needs to be 38: whl of lightsim were not released for 3.10 at the time + resource_class: small + steps: + - checkout + - run: + command: | + apt-get update + apt-get install -y coinor-cbc + - run: python -m pip install virtualenv + - run: python -m virtualenv venv_test + - run: + command: | + source venv_test/bin/activate + python -m pip install -U pip setuptools wheel "numpy==1.26.4" + - run: + command: | + source venv_test/bin/activate + python -m pip install -e .[chronix2grid] "linopy==0.3.8" "scs==3.2.4.post1" "ecos==2.0.13" "pillow==10.3.0" "numpy==1.26.4" "xarray==2024.3.0" + pip freeze + - run: + command: | + source venv_test/bin/activate + export _GRID2OP_FORCE_TEST=1 + python -m unittest grid2op/tests/fromChronix2grid.py + install39: executor: python39 resource_class: small @@ -232,8 +258,7 @@ jobs: command: | export _GRID2OP_FORCE_TEST=1 source venv_test/bin/activate - python -m pip install -U pip setuptools wheel "numpy>=1.20,<1.21" "pandas<2.2" "scipy==1.10.1" numba - python -m pip install "chronix2grid>=1.1.0.post1" "gymnasium==0.26.3" "matplotlib==3.7.5" "xarray==2023.10.0" "scs==3.0.0" "ecos==2.0.0" + python -m pip install -U pip setuptools wheel "numpy>=1.20,<1.21" "pandas<2.2" "scipy==1.10.1" "pillow<10.4.0" numba python -m pip uninstall -y grid2op - run: command: | # issue with previous more simple install, so I fix some versions @@ -249,7 +274,7 @@ jobs: - run: command: | source venv_test/bin/activate - python -m pip install "numpy>=1.26,<1.27" "pandas<2.2" "scipy<1.12" numba + python -m pip install "numpy>=1.26,<1.27" "pandas<2.2" "scipy<1.12" numba "pillow<10.4.0" pip freeze - run: command: | @@ -369,6 +394,8 @@ workflows: - test - legacy_lightsim_old_pp - legacy_lightsim + - test_chronix2grid + install: jobs: - install38 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a41c23e11..1e8054ad0 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -12,6 +12,8 @@ jobs: name: Build linux ${{ matrix.python.name }} wheel runs-on: ubuntu-latest container: quay.io/pypa/manylinux2014_x86_64 + env: + ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: true strategy: matrix: python: @@ -59,7 +61,7 @@ jobs: - name: Build wheel run: | - python3 setup.py bdist_wheel + python setup.py bdist_wheel # auditwheel repair dist/*.whl # only for compiled code ! - name: Install wheel @@ -69,12 +71,16 @@ jobs: - name: Check package can be imported run: | - python3 -c "import grid2op" - python3 -c "from grid2op import *" - python3 -c "from grid2op.Action._backendAction import _BackendAction" - + python -c "import grid2op" + python -c "from grid2op import *" + python -c "from grid2op.Action._backendAction import _BackendAction" + + - name: List wheel + run: + ls ./dist/*.whl + - name: Upload wheel - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: grid2op-wheel-${{ matrix.config.name }}-${{ matrix.python.name }} path: dist/*.whl @@ -165,10 +171,70 @@ jobs: name: grid2op-sources path: dist/*.tar.gz + auto_class_in_file: + name: Test ${{ matrix.config.name }} OS can handle automatic class generation + runs-on: ${{ matrix.config.os }} + strategy: + matrix: + config: + - { + name: darwin, + os: macos-latest, + } + # - { + # name: windows, + # os: windows-2019, + # } + - { + name: ubuntu, + os: ubuntu-latest, + } + python: + - { + name: cp39, + version: '3.9', + } + - { + name: cp312, + version: '3.12', + } + + steps: + + - name: Checkout sources + uses: actions/checkout@v1 + with: + submodules: true + + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python.version }} + + - name: Install Python dependencies + run: | + python -m pip install --upgrade pip + python -m pip install --upgrade wheel + python -m pip install --upgrade setuptools + python -m pip install --upgrade gymnasium "numpy<2" + + - name: Build wheel + run: python setup.py bdist_wheel + + - name: Install wheel + shell: bash + run: | + python -m pip install dist/*.whl --user + pip freeze + + - name: Test the automatic generation of classes in the env folder + run: | + python -m unittest grid2op/tests/automatic_classes.py -f + package: name: Test install runs-on: ubuntu-latest - needs: [manylinux_build, macos_windows_build] + needs: [manylinux_build, macos_windows_build, auto_class_in_file] steps: - name: Download wheels diff --git a/.gitignore b/.gitignore index 595222640..6bd200b60 100644 --- a/.gitignore +++ b/.gitignore @@ -413,6 +413,9 @@ getting_started/env_py38_grid2op110_ray210.ipynb grid2op/tests/req_chronix2grid grid2op/tests/venv_test_chronix2grid/ getting_started/venv_310_ray/ +grid2op/tests/venv_test_autoclass/ +test_eduardo.py +grid2op/tests/failed_test* # profiling files **.prof diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 3f3225e69..3acf1eaa0 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -40,20 +40,31 @@ Work kind of in progress Next release --------------------------------- +- numpy 2 compat (need pandapower for that) +- automatic read from local dir also on windows ! +- TODO doc for the "new" feature of automatic "experimental_read_from_local_dir" - TODO bug on maintenance starting at midnight (they are not correctly handled in the observation) => cf script test_issue_616 +- TODO put the Grid2opEnvWrapper directly in grid2op as GymEnv +- TODO faster gym_compat (especially for DiscreteActSpace and BoxGymObsSpace) - TODO Notebook for tf_agents - TODO Notebook for acme - TODO Notebook using "keras rl" (see https://keras.io/examples/rl/ppo_cartpole/) -- TODO put the Grid2opEnvWrapper directly in grid2op as GymEnv - TODO example for MCTS https://github.com/bwfbowen/muax et https://github.com/google-deepmind/mctx - TODO jax everything that can be: create a simple env based on jax for topology manipulation, without redispatching or rules - TODO backend in jax, maybe ? - TODO done and truncated properly handled in gym_compat module (when game over before the end it's probably truncated and not done) - -[1.10.3] - 2024-xx-yy +- TODO when reset, have an attribute "reset_infos" with some infos about the + way reset was called. +- TODO ForecastEnv in MaskedEnv ! (and obs.simulate there too !) +- TODO finish the test in automatic_classes +- TODO in multi-mix increase the reset options with the mix the user wants +- TODO L2RPN scores as reward (sum loads after the game over and have it in the final reward) +- TODO on CI: test only gym, only gymnasium and keep current test for both gym and gymnasium + +[1.10.3] - 2024-07-yy ------------------------- - TODO Automatic "experimental_read_from_local_dir" @@ -66,8 +77,20 @@ Next release use it (will likely have no effect). Prefer using `env.set_max_iter` instead. - [BREAKING] now the `runner.run()` method only accept kwargs argument (because it should always have been like this) +- [BREAKING] to improve pickle support and multi processing capabilities, the attribute + `gym_env.observation_space._init_env` and `gym_env.observation_space.initial_obs_space` + have been deleted (for the `Dict` space only, for the other spaces like the `Box` they + were not present in the first place) +- [BREAKING] in the `GymEnv` class now by default the underlying grid2op environment has no + forecast anymore in an attempt to make this wrapper faster AND more easily pickle-able. You can + retrieve the old behaviour by passing `gym_env = GymEnv(grid2op_env, with_forecast=True)` - [FIXED] a bug in the `MultiFolder` and `MultifolderWithCache` leading to the wrong computation of `max_iter` on some corner cases +- [FIXED] the function `cleanup_action_space()` did not work correctly when the "chronics_hander" + was not initialized for some classes +- [FIXED] the `_observationClass` attribute of the "observation env" (used for simulate and forecasted env) + is now an Observation and not an Action. +- [FIXED] a bug when deep copying an "observation environment" (it changes its class) - [FIXED] issue on `seed` and `MultifolderWithCache` which caused https://github.com/rte-france/Grid2Op/issues/616 - [FIXED] another issue with the seeding of `MultifolderWithCache`: the seed was not used @@ -81,18 +104,36 @@ Next release even before the "time series" are applied (and before the user defined thermal limits were set) which could lead to disconnected powerlines even before the initial step (t=0, when time series are loaded) +- [FIXED] an issue with the "max_iter" for `FromNPY` time series generator +- [FIXED] a bug in `MultiMixEnvironment` : a multi-mix could be created even if the underlying + powergrids (for each mix) where not the same. +- [FIXED] a bug in `generate_classes` (experimental_read_from_local_dir) with alert data. +- [FIXED] a bug in the `Runner` when using multi processing on macos and windows OS: some non default + parameters where not propagated in the "child" process (bug in `runner._ger_params`) - [ADDED] possibility to skip some step when calling `env.reset(..., options={"init ts": ...})` - [ADDED] possibility to limit the duration of an episode with `env.reset(..., options={"max step": ...})` - [ADDED] possibility to specify the "reset_options" used in `env.reset` when using the runner with `runner.run(..., reset_options=xxx)` -- [ADDED] the time series now are able to regenerate their "random" part +- [ADDED] the argument `mp_context` when building the runner to help pass a multiprocessing context in the + grid2op `Runner` +- [ADDED] the time series are now able to regenerate their "random" part even when "cached" thanks to the addition of the `regenerate_with_new_seed` of the `GridValue` class (in public API) - [ADDED] `MultifolderWithCache` now supports `FromHandlers` time series generator +- [IMPROVED] more consistency in the way the classes are initialized at the creation of an environment +- [IMPROVED] more consistency when an environment is copied (some attributes of the copied env were + deep copied incorrectly) +- [IMPROVED] Doc about the runner - [IMPROVED] the documentation on the `time series` folder. - [IMPROVED] now the "maintenance from json" (*eg* the `JSONMaintenanceHandler` or the `GridStateFromFileWithForecastsWithMaintenance`) can be customized with the day of the week where the maintenance happens (key `maintenance_day_of_week`) +- [IMPROVED] in case of "`MultiMixEnvironment`" there is now only class generated for + all the underlying mixes (instead of having one class per mixes) +- [IMPROVED] the `EpisodeData` have now explicitely a mode where they can be shared accross + processes (using `fork` at least), see `ep_data.make_serializable` +- [IMPROVED] chronix2grid tests are now done independantly on the CI + [1.10.2] - 2024-05-27 ------------------------- @@ -885,7 +926,7 @@ Next release `Issue#185 `_ ) - [IMPROVED] the seed of openAI gym for composed action space (see issue `https://github.com/openai/gym/issues/2166`): in waiting for an official fix, grid2op will use the solution proposed there - https://github.com/openai/gym/issues/2166#issuecomment-803984619 ) + https://github.com/openai/gym/issues/2166#issuecomment-803984619 [1.5.1] - 2021-04-15 ----------------------- diff --git a/docs/grid2op.rst b/docs/grid2op.rst index 1e115f329..02fc4826d 100644 --- a/docs/grid2op.rst +++ b/docs/grid2op.rst @@ -447,6 +447,7 @@ alert (when the attack is happening) Disclaimer ----------- + Grid2op is a research testbed platform, it has not been tested in "production" context Going further @@ -458,3 +459,5 @@ more information and a detailed tour about the issue that grid2op tries to addre .. note:: As of writing (december 2020) most of these notebooks focus on the "agent" part of grid2op. We would welcome any contribution to better explain the other aspect of this platform. + +.. include:: final.rst diff --git a/docs/gym.rst b/docs/gym.rst index 06fe365f7..02e47d796 100644 --- a/docs/gym.rst +++ b/docs/gym.rst @@ -504,37 +504,7 @@ This is because grid2op will (to save computation time) generate some classes (t fly, once the environment is loaded. And unfortunately, pickle module is not always able to process these (meta) data. -Try to first create (automatically!) the files containing the description of the classes -used by your environment (for example): - -.. code-block:: python - - from grid2op import make - from grid2op.Reward import RedispReward - from lightsim2grid import LightSimBackend - - env_name = 'l2rpn_wcci_2022' - backend_class = LightSimBackend - env = make(env_name, reward_class=RedispReward, backend=backend_class()) - env.generate_classes() - -.. note:: - This piece of code is to do once (each time you change the backend or the env name) - -And then proceed as usual by loading the grid2op environment -with the key-word `experimental_read_from_local_dir` - -.. code-block:: python - - from grid2op import make - from grid2op.Reward import RedispReward - from lightsim2grid import LightSimBackend - - env_name = 'l2rpn_wcci_2022' - backend_class = LightSimBackend - env = make(env_name, reward_class=RedispReward, backend=backend_class(), - experimental_read_from_local_dir=True) - # do whatever +You can solve this issue by look at :ref:`troubleshoot_pickle` section of the documentation. Observation XXX outside given space YYY **************************************** @@ -560,4 +530,4 @@ Detailed Documentation by class :members: :autosummary: -.. include:: final.rst \ No newline at end of file +.. include:: final.rst diff --git a/docs/index.rst b/docs/index.rst index 31dd1f648..42179d3b4 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -75,6 +75,7 @@ Environments create_an_environment dive_into_time_series data_pipeline + troubleshoot Usage examples --------------------- diff --git a/docs/model_based.rst b/docs/model_based.rst index 3645fa6e9..5bd373985 100644 --- a/docs/model_based.rst +++ b/docs/model_based.rst @@ -378,3 +378,5 @@ And for the `ExampleAgent2`: res = strat[0] # action is the first one of the best strategy highest_score = ts_survived return res + +.. include:: final.rst diff --git a/docs/model_free.rst b/docs/model_free.rst index db326736f..94f8f7458 100644 --- a/docs/model_free.rst +++ b/docs/model_free.rst @@ -17,3 +17,5 @@ Some examples are given in "l2rpn-baselines": - `PPO with RLLIB `_ - `PPO with stable-baselines3 `_ + +.. include:: final.rst diff --git a/docs/observation.rst b/docs/observation.rst index 97a881108..05bb35a75 100644 --- a/docs/observation.rst +++ b/docs/observation.rst @@ -133,4 +133,4 @@ Detailed Documentation by class :special-members: :autosummary: -.. include:: final.rst \ No newline at end of file +.. include:: final.rst diff --git a/docs/optimization.rst b/docs/optimization.rst index 24a58e304..ba9407a8e 100644 --- a/docs/optimization.rst +++ b/docs/optimization.rst @@ -19,3 +19,5 @@ Basically an "optimizer" agent looks like (from a very high level): 3) update the "formulation" using the observation received 4) run a solver to solve the "problem" 5) convert back the "decisions" (output) of the solver into a "grid2op" action + +.. include:: final.rst diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 3a641da06..3955b8182 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -121,3 +121,5 @@ The most basic code, for those familiar with openAI gym (a well-known framework To make the use of grid2op alongside grid2op environment easier, we developed a module described in :ref:`openai-gym`. + +.. include:: final.rst diff --git a/docs/runner.rst b/docs/runner.rst index 266c26c2d..2752971cc 100644 --- a/docs/runner.rst +++ b/docs/runner.rst @@ -125,6 +125,189 @@ For information, as of writing (march 2021): - macOS with python <= 3.7 will behave like any python version on linux - windows and macOS with python >=3.8 will behave differently than linux but similarly to one another +Some common runner options: +------------------------------- + +Specify an agent instance and not a class +******************************************* + +By default, if you specify an agent class (*eg* `AgentCLS`), then the runner will initialize it with: + +.. code-block:: python + + agent = AgentCLS(env.action_space) + +But you might want to use agent initialized in a more complex way. To that end, you can customize the +agent instance you want to use (and not only its class) with the following code: + +.. code-block:: python + + import grid2op + from grid2op.Agent import RandomAgent # for example... + from grid2op.Runner import Runner + + env = grid2op.make("l2rpn_case14_sandbox") + + agent_instance = RandomAgent(env.action_space) + runner = Runner(**env.get_params_for_runner(), agentClass=None, agentInstance=agent_instance) + res = runner.run(nb_episode=nn_episode) + +Customize the scenarios +************************** + +You can customize the seeds, the scenarios ID you want, the number of initial steps to skip, the +maximum duration of an episode etc. For more information, please refer to the :func:`Runner.run` +for more information. But basically, you can do: + +.. code-block:: python + + import grid2op + from grid2op.Agent import RandomAgent # for example... + from grid2op.Runner import Runner + + env = grid2op.make("l2rpn_case14_sandbox") + + agent_instance = RandomAgent(env.action_space) + runner = Runner(**env.get_params_for_runner(), agentClass=None, agentInstance=agent_instance) + res = runner.run(nb_episode=nn_episode, + + # nb process to use + nb_process=1, + + # path where the outcome will be saved + path_save=None, + + # max number of steps in an environment + max_iter=None, + + # progress bar to use + pbar=False, + + # seeds to use for the environment + env_seeds=None, + + # seeds to use for the agent + agent_seeds=None, + + # id the time serie to use + episode_id=None, + + # whether to add the outcome (EpisodeData) as a result of this function + add_detailed_output=False, + + # whether to keep track of the number of call to "high resolution simulator" + # (eg obs.simulate or obs.get_forecasted_env) + add_nb_highres_sim=False, + + # which initial state you want the grid to be in + init_states=None, + + # options passed in `env.reset(..., options=XXX)` + reset_options=None, + ) + + +Retrieve what has happened +**************************** + +You can also easily retrieve the :class:`grid2op.Episode.EpisodeData` representing your runs with: + +.. code-block:: python + + import grid2op + from grid2op.Agent import RandomAgent # for example... + from grid2op.Runner import Runner + + env = grid2op.make("l2rpn_case14_sandbox") + + agent_instance = RandomAgent(env.action_space) + runner = Runner(**env.get_params_for_runner(), agentClass=None, agentInstance=agent_instance) + res = runner.run(nb_episode=2, + add_detailed_output=True) + for *_, ep_data in res: + # ep_data are the EpisodeData you can use to do whatever + ... + +Save the results +***************** + +You can save the results in a standardized format with: + +.. code-block:: python + + import grid2op + from grid2op.Agent import RandomAgent # for example... + from grid2op.Runner import Runner + + env = grid2op.make("l2rpn_case14_sandbox") + + agent_instance = RandomAgent(env.action_space) + runner = Runner(**env.get_params_for_runner(), + agentClass=None, + agentInstance=agent_instance) + res = runner.run(nb_episode=2, + save_path="A/PATH/SOMEWHERE") # eg "/home/user/you/grid2op_results/this_run" + +Multi processing +*********************** + +You can also easily (on some platform) easily make the evaluation faster by using the "multi processing" python +package with: + +.. code-block:: python + + import grid2op + from grid2op.Agent import RandomAgent # for example... + from grid2op.Runner import Runner + + env = grid2op.make("l2rpn_case14_sandbox") + + agent_instance = RandomAgent(env.action_space) + runner = Runner(**env.get_params_for_runner(), + agentClass=None, + agentInstance=agent_instance) + res = runner.run(nb_episode=2, + nb_process=2) + +Customize the multi processing +******************************** + +And, as of grid2op 1.10.3 you can know customize the multi processing context you want +to use to evaluate your agent, like this: + +.. code-block:: python + + import multiprocessing as mp + import grid2op + from grid2op.Agent import RandomAgent # for example... + from grid2op.Runner import Runner + + env = grid2op.make("l2rpn_case14_sandbox") + + agent_instance = RandomAgent(env.action_space) + + ctx = mp.get_context('spawn') # or "fork" or "forkserver" + runner = Runner(**env.get_params_for_runner(), + agentClass=None, + agentInstance=agent_instance, + mp_context=ctx) + res = runner.run(nb_episode=2, + nb_process=2) + +If you set this, the multiprocessing `Pool` used to evaluate your agents will be made with: + +.. code-block:: python + + with mp_context.Pool(nb_process) as p: + .... + +Otherwise the default "Pool" is used: + +.. code-block:: python + + with Pool(nb_process) as p: + .... + Detailed Documentation by class ------------------------------- diff --git a/docs/timeserie_handlers.rst b/docs/timeserie_handlers.rst index e7a9b1fb5..bd76abddf 100644 --- a/docs/timeserie_handlers.rst +++ b/docs/timeserie_handlers.rst @@ -344,3 +344,5 @@ Detailed Documentation by class .. automodule:: grid2op.Chronics.handlers :members: :autosummary: + +.. include:: final.rst diff --git a/docs/troubleshoot.rst b/docs/troubleshoot.rst new file mode 100644 index 000000000..fbfec1fc4 --- /dev/null +++ b/docs/troubleshoot.rst @@ -0,0 +1,189 @@ + +.. _troubleshoot_page: + +Known issues and workarounds +=============================== + + +In this section we will detail what are the common questions we have regarding grid2op and how to +best solve them (if we are aware of such a way...) + +.. _troubleshoot_pickle: + +Pickle issues +-------------------------- + +The most common (and oldest) issue regarding grid2op is its interaction with the `pickle` module +in python. + +This module is used internally by the `multiprocessing` module and many others. + +By default (and "by design") grid2op will create the classes when an environment +is loaded. You can notice it like this: + +.. code-block:: python + + import grid2op + + env_name = "l2rpn_case14_sandbox" + env = grid2op.make(env_name) + + print(type(env)) + +This will show something like `Environment_l2rpn_case14_sandbox`. This means that, +not only the object `env` is created when you call `grid2op.make` but also +the class that `env` belongs too (in this case `Environment_l2rpn_case14_sandbox`). + +.. note:: + We decided to adopt this design so that the powergrid reprensentation in grid2op + is not copied and can be access pretty easily from pretty much every objects. + + For example you can call `env.n_gen`, `type(env).n_gen`, `env.backend.n_gen`, + `type(env.backend).n_gen`, `obs.n_gen`, `type(obs).n_gen`, `act.n_gen`, + `type(act).n_gen`, `env.observation_space.n_gen`, `type(env.observation_space).n_gen` + well... you get the idea + + But allowing so makes it "hard" for python to understand how to transfer objects + from one "process" to another or to save / restore it (indeed, python does not + save the entire class definition it only saves the class names.) + +This type of issue takes the form of an error with: + +- `XXX_env_name` (*eg* `CompleteObservation_l2rpn_wcci_2022`) is not serializable. +- `_pickle.PicklingError`: Can't pickle : attribute lookup _ObsEnv_l2rpn_case14_sandbox on abc failed + +Automatic 'class_in_file' ++++++++++++++++++++++++++++ + +To solve this issue, we are starting from grid2op 1.10 to introduce some ways +to get around this automatically. It will be integrated incrementally to make +sure not to break any previous code. + +The main idea is that grid2op will define the class as it used to (no change there) +but instead of keeping them "in memory" it will write it on the hard drive (in +a folder within the environment data) each time an environment is created. + +This way, when pickle or multiprocessing will attempt to load the environment class, +they will be able to because the files are stored on the hard drive. + +There are some drawbacks of course. The main one being that creating an environment +can take a bit more time (especially if you have slow I/O). It will also use +a bit of disk space (a few kB so nothing to worry about). + +For now we tested it on multi processing and it gives promising results. + +**TL;DR**: Enable this feature by calling `grid2op.make(env_name, class_in_file=True)` and you're good to go. + +To enable this, you can: + +- define a default behaviour by editing the `~/.grid2opconfig.json` global parameters +- define the environment variable `grid2op_class_in_file` **BEFORE** importing grid2op +- use the kwargs `class_in_file` when calling the `grid2op.make` function + +.. note:: + In case of "conflicting" instruction grid2op will do the following: + + - if `class_in_file` is provided in the call to `grid2op.make(...)` it will use this and ignore everything else + - (else) if the environment variable `grid2op_class_in_file` is defined, grid2op will use it + - (else) if the configuration file is present and the key `class_in_file` is there, grid2op will + use it + - (else) it will use its default behaviour (as of writing, grid2op 1.10.3) it is to **DEACTIVATE** + this feature (in the near future the default will change and it will be activated by default) + +For example: + +The file `~/.grid2opconfig.json` can look like: + +.. code-block:: json + + { + "class_in_file" : false + } + +or +.. code-block:: json + + { + "class_in_file" : true + } + +If you prefer to work with environment variables, we recommend you do something like : + +.. code-block:: python + + import os + + os.environ["grid2op_class_in_file"] = "true" # or "false" if you want to disable it + + import grid2op + +And if you prefer to use it directly in `grid2op.make(...)` funciton, you can do it with: + +.. code-block:: python + + import grid2op + env_name = "l2rpn_case14_sandbox" + env = grid2op.make(env_name, class_in_file=True) # or `class_in_file=False` + + +If you want to know if you environment has used this new feature, you can check with: + +.. code-block:: python + + import grid2op + env = grid2op.make(...) + print(env.classes_are_in_files()) + +.. danger:: + If you use this, make sure (for now) that the original grid2op environment that you have created + is not deleted. If that is the case then the folder containing the classes definition will be + removed and you might not be able to work with grid2op correctly. + + +Experimental `read_from_local_dir` ++++++++++++++++++++++++++++++++++++ + +Before grid2op 1.10.3 the only way to get around pickle / multiprocessing issue was a "two stage" process: +you had first to tell grid2op to generate the classes and then to tell it to use it in all future environment. + +This had the drawbacks that if you changed the backend classes, or the observation classes or the +action classes, you needed to start the whole process again. ANd it as manual so you might have ended up +doing some un intended actions which could create some "silent bugs" (the worst kind, like for example +not using the right class...) + +To do it you first needed to call, once (as long as you did not change backend class or observation or action etc.) +in a **SEPARATE** python script: + +.. code-block:: python + + import grid2op + env_name = "l2rpn_case14_sandbox" # or any other name + + env = grid2op.make(env_name, ...) # again: redo this step each time you customize "..." + # for example if you change the `action_class` or the `backend` etc. + + env.generate_classes() + + +And then, in another script, the main one you want to use: + +.. code-block:: python + + import grid2op + env_name = SAME NAME AS ABOVE + env = grid2op.make(env_name, + experimental_read_from_local_dir=True, + SAME ENV CUSTOMIZATION AS ABOVE) + +As of grid2op 1.10.3 this process can be made automatically (not without some drawbacks, see above). It might +interact in a weird (and unpredictable) way with the `class_in_file` so we would recommend to use one **OR** +(exclusive OR, XOR for the mathematicians) the other but avoid mixing the two: + +- either use `grid2op.make(..., class_in_file=True)` +- or use `grid2op.make(..., experimental_read_from_local_dir=True)` + +Thus we **DO NOT** recommend to use something like +`grid2op.make(..., experimental_read_from_local_dir=True, class_in_file=True)` + + +.. include:: final.rst diff --git a/docs/utils.rst b/docs/utils.rst index fde3a084a..d30de21a1 100644 --- a/docs/utils.rst +++ b/docs/utils.rst @@ -22,4 +22,3 @@ Detailed Documentation by class :autosummary: .. include:: final.rst - diff --git a/docs/voltagecontroler.rst b/docs/voltagecontroler.rst index 19e391297..1c85a3552 100644 --- a/docs/voltagecontroler.rst +++ b/docs/voltagecontroler.rst @@ -41,4 +41,4 @@ Detailed Documentation by class :members: :autosummary: -.. include:: final.rst \ No newline at end of file +.. include:: final.rst diff --git a/getting_started/00_Introduction.ipynb b/getting_started/00_Introduction.ipynb index bf16a50db..a58f23b7b 100644 --- a/getting_started/00_Introduction.ipynb +++ b/getting_started/00_Introduction.ipynb @@ -191,10 +191,25 @@ "\n", "- In reality there can also be \"switches\" that can connect the two busbars (reconfiguring the topology of the substation can be done with only one switch, but on the other hand, sometimes changing one switch will have no effect at all).\n", "\n", - "- You can also have more than 2 busbars in each substation (sometimes 5 or 6 for example). This makes the number of possible topologies even higher than what it is in grid2op.\n", + "- You can also have more than 2 busbars in each substation (sometimes 5 or 6 for example). This makes the number of possible topologies even higher than it currently is in grid2op (see below for some additional precisions).\n", "\n", "- Finally, most of the time a single busbar count a \"switch\" in its middle that allows to disconnect part of the element connected to it to another part. Basically this entails that some combinaison of elements are not possible to perform\n", "\n", + "*Additional precisions about the number of independant busbsars per susbtations*: Starting from grid2op 1.10.2 you can now have any number of busbars you want per susbtations. For example, you can create an environment with:\n", + "```python\n", + "env = grid2op.make(\"l2rpn_case14_sandbox\")\n", + "```\n", + "To have the default of 2 busbars per susbtations. But you can also do:\n", + "```python\n", + "env_3 = grid2op.make(\"l2rpn_case14_sandbox\", n_busbar=3)\n", + "```\n", + "Then you end-up with 3 busbars for all substations or you can even do:\n", + "```python\n", + "env_1 = grid2op.make(\"l2rpn_case14_sandbox\", n_busbar=1)\n", + "# or\n", + "env_10 = grid2op.make(\"l2rpn_case14_sandbox\", n_busbar=10)\n", + "```\n", + "\n", "And of course, we model explicitly in this framework (*eg* we allow the agents to act on) only some elements of a powergrid. In reality, much more heterogeneous objects exists with more complex properties. \n", "\n", "We decided to make all these assumptions because we thought it was the easiest setting that allow to perform some topological reconfiguration, beside connecting / disconnecting powerlines.\n", diff --git a/grid2op/Action/actionSpace.py b/grid2op/Action/actionSpace.py index 4ce24be68..2b55406e0 100644 --- a/grid2op/Action/actionSpace.py +++ b/grid2op/Action/actionSpace.py @@ -44,6 +44,7 @@ def __init__( gridobj, legal_action, actionClass=BaseAction, # need to be a base grid2op type (and not a type generated on the fly) + _local_dir_cls=None, ): """ INTERNAL USE ONLY @@ -71,7 +72,7 @@ def __init__( """ actionClass._add_shunt_data() actionClass._update_value_set() - SerializableActionSpace.__init__(self, gridobj, actionClass=actionClass) + SerializableActionSpace.__init__(self, gridobj, actionClass=actionClass, _local_dir_cls=_local_dir_cls) self.legal_action = legal_action def __call__( diff --git a/grid2op/Action/serializableActionSpace.py b/grid2op/Action/serializableActionSpace.py index f1c8bbee7..79f409336 100644 --- a/grid2op/Action/serializableActionSpace.py +++ b/grid2op/Action/serializableActionSpace.py @@ -54,7 +54,7 @@ class SerializableActionSpace(SerializableSpace): '"which is not the type of action handled by this action space "' '("{}")') - def __init__(self, gridobj, actionClass=BaseAction, _init_grid=True): + def __init__(self, gridobj, actionClass=BaseAction, _init_grid=True, _local_dir_cls=None): """ INTERNAL USE ONLY @@ -74,7 +74,10 @@ def __init__(self, gridobj, actionClass=BaseAction, _init_grid=True): """ SerializableSpace.__init__( - self, gridobj=gridobj, subtype=actionClass, _init_grid=_init_grid + self, gridobj=gridobj, + subtype=actionClass, + _init_grid=_init_grid, + _local_dir_cls=_local_dir_cls ) self.actionClass = self.subtype self._template_act = self.actionClass() diff --git a/grid2op/Backend/backend.py b/grid2op/Backend/backend.py index 9bcaa1613..3e875e6ad 100644 --- a/grid2op/Backend/backend.py +++ b/grid2op/Backend/backend.py @@ -2008,8 +2008,9 @@ def update_from_obs(self, '"grid2op.Observation.CompleteObservation".' ) - backend_action = self.my_bk_act_class() - act = self._complete_action_class() + cls = type(self) + backend_action = cls.my_bk_act_class() + act = cls._complete_action_class() line_status = self._aux_get_line_status_to_set(obs.line_status) # skip the action part and update directly the backend action ! dict_ = { @@ -2023,7 +2024,7 @@ def update_from_obs(self, }, } - if type(self).shunts_data_available and type(obs).shunts_data_available: + if cls.shunts_data_available and type(obs).shunts_data_available: if "_shunt_bus" not in type(obs).attr_list_set: raise BackendError( "Impossible to set the backend to the state given by the observation: shunts data " @@ -2040,13 +2041,13 @@ def update_from_obs(self, sh_q[~shunt_co] = np.NaN dict_["shunt"]["shunt_p"] = sh_p dict_["shunt"]["shunt_q"] = sh_q - elif type(self).shunts_data_available and not type(obs).shunts_data_available: + elif cls.shunts_data_available and not type(obs).shunts_data_available: warnings.warn("Backend supports shunt but not the observation. This behaviour is non standard.") act.update(dict_) backend_action += act self.apply_action(backend_action) - def assert_grid_correct(self) -> None: + def assert_grid_correct(self, _local_dir_cls=None) -> None: """ INTERNAL @@ -2055,9 +2056,6 @@ def assert_grid_correct(self) -> None: This is done as it should be by the Environment """ - # lazy loading - from grid2op.Action import CompleteAction - from grid2op.Action._backendAction import _BackendAction if hasattr(self, "_missing_two_busbars_support_info"): if self._missing_two_busbars_support_info: @@ -2081,23 +2079,21 @@ def assert_grid_correct(self) -> None: warnings.warn("Your backend is missing the `_missing_two_busbars_support_info` " "attribute. This is known issue in lightims2grid <= 0.7.5. Please " "upgrade your backend. This will raise an error in the future.") - + orig_type = type(self) - if orig_type.my_bk_act_class is None: + if orig_type.my_bk_act_class is None and orig_type._INIT_GRID_CLS is None: + # NB the second part of the "if": `orig_type._INIT_GRID_CLS is None` + # has been added in grid2Op 1.10.3 to handle multiprocessing correctly: + # classes passed in multi processing should not be initialized a second time + # class is already initialized # and set up the proper class and everything self._init_class_attr() - - # hack due to changing class of imported module in the module itself + future_cls = orig_type.init_grid( - type(self), force_module=type(self).__module__ + type(self), _local_dir_cls=_local_dir_cls ) self.__class__ = future_cls - setattr( - sys.modules[type(self).__module__], - self.__class__.__name__, - self.__class__, - ) # reset the attribute of the grid2op.Backend.Backend class # that can be messed up with depending on the initialization of the backend @@ -2108,13 +2104,21 @@ def assert_grid_correct(self) -> None: orig_type._clear_grid_dependant_class_attributes() my_cls = type(self) - my_cls.my_bk_act_class = _BackendAction.init_grid(my_cls) - my_cls._complete_action_class = CompleteAction.init_grid(my_cls) - my_cls._complete_action_class._add_shunt_data() - my_cls._complete_action_class._update_value_set() - my_cls.assert_grid_correct_cls() + my_cls._add_internal_classes(_local_dir_cls) self._remove_my_attr_cls() + @classmethod + def _add_internal_classes(cls, _local_dir_cls): + # lazy loading + from grid2op.Action import CompleteAction + from grid2op.Action._backendAction import _BackendAction + + cls.my_bk_act_class = _BackendAction.init_grid(cls, _local_dir_cls=_local_dir_cls) + cls._complete_action_class = CompleteAction.init_grid(cls, _local_dir_cls=_local_dir_cls) + cls._complete_action_class._add_shunt_data() + cls._complete_action_class._update_value_set() + cls.assert_grid_correct_cls() + def _remove_my_attr_cls(self): """ INTERNAL diff --git a/grid2op/Chronics/chronicsHandler.py b/grid2op/Chronics/chronicsHandler.py index 08004ce73..9f04c8f92 100644 --- a/grid2op/Chronics/chronicsHandler.py +++ b/grid2op/Chronics/chronicsHandler.py @@ -235,5 +235,7 @@ def cleanup_action_space(self): """INTERNAL, used to forget the "old" action_space when the chronics_handler is copied for example. """ + if self._real_data is None: + return self._real_data.cleanup_action_space() \ No newline at end of file diff --git a/grid2op/Chronics/fromNPY.py b/grid2op/Chronics/fromNPY.py index 4d34f80ac..475f5aa7e 100644 --- a/grid2op/Chronics/fromNPY.py +++ b/grid2op/Chronics/fromNPY.py @@ -222,6 +222,7 @@ def __init__( ) self._init_state = init_state + self._max_iter = min(self._i_end - self._i_start, load_p.shape[0]) def initialize( self, @@ -252,6 +253,7 @@ def initialize( self.curr_iter = 0 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): # get the "long hash" from blake2b @@ -420,6 +422,7 @@ def next_chronics(self): # update the forecast self._forecasts.next_chronics() self.check_validity(backend=None) + self._max_iter = self._i_end - self._i_start def done(self): """ @@ -648,6 +651,7 @@ def change_i_start(self, new_i_start: Union[int, None]): self.__new_istart = int(new_i_start) else: self.__new_istart = None + def change_i_end(self, new_i_end: Union[int, None]): """ diff --git a/grid2op/Chronics/multiFolder.py b/grid2op/Chronics/multiFolder.py index cf34829c8..e8b8c9b48 100644 --- a/grid2op/Chronics/multiFolder.py +++ b/grid2op/Chronics/multiFolder.py @@ -792,4 +792,6 @@ def get_init_action(self, names_chronics_to_backend: Optional[Dict[Literal["load def cleanup_action_space(self): super().cleanup_action_space() + if self.data is None: + return self.data.cleanup_action_space() diff --git a/grid2op/Converter/BackendConverter.py b/grid2op/Converter/BackendConverter.py index dfd0ced63..4c023c85e 100644 --- a/grid2op/Converter/BackendConverter.py +++ b/grid2op/Converter/BackendConverter.py @@ -429,7 +429,7 @@ def _auto_fill_vect_topo_aux(self, n_elem, source_pos, target_pos, sr2tg): self._topo_tg2sr[source_pos[sr2tg]] = target_pos self._topo_sr2tg[target_pos] = source_pos[sr2tg] - def assert_grid_correct(self): + def assert_grid_correct(self, _local_dir_cls=None) -> None: # this is done before a call to this function, by the environment tg_cls = type(self.target_backend) sr_cls = type(self.source_backend) @@ -480,13 +480,13 @@ def assert_grid_correct(self): ) # init the target backend (the one that does the computation and that is initialized) - self.target_backend.assert_grid_correct() + self.target_backend.assert_grid_correct(_local_dir_cls=_local_dir_cls) # initialize the other one, because, well the grid should be seen from both backend self.source_backend._init_class_attr(obj=self) - self.source_backend.assert_grid_correct() + self.source_backend.assert_grid_correct(_local_dir_cls=_local_dir_cls) # and this should be called after all the rest - super().assert_grid_correct() + super().assert_grid_correct(_local_dir_cls=_local_dir_cls) # everything went well, so i can properly terminate my initialization self._init_myself() diff --git a/grid2op/Converter/IdToAct.py b/grid2op/Converter/IdToAct.py index be96e992d..063b1f59d 100644 --- a/grid2op/Converter/IdToAct.py +++ b/grid2op/Converter/IdToAct.py @@ -70,6 +70,7 @@ class IdToAct(Converter): def __init__(self, action_space): Converter.__init__(self, action_space) self.__class__ = IdToAct.init_grid(action_space) + self.init_action_space = action_space self.all_actions = [] # add the do nothing topology self.all_actions.append(super().__call__()) diff --git a/grid2op/Environment/_forecast_env.py b/grid2op/Environment/_forecast_env.py index ad08fc7df..7378df7c7 100644 --- a/grid2op/Environment/_forecast_env.py +++ b/grid2op/Environment/_forecast_env.py @@ -21,6 +21,7 @@ def __init__(self, *args, **kwargs): if "_update_obs_after_reward" not in kwargs: kwargs["_update_obs_after_reward"] = False super().__init__(*args, **kwargs) + self._do_not_erase_local_dir_cls = True def step(self, action: BaseAction) -> Tuple[BaseObservation, float, bool, dict]: self._highres_sim_counter += 1 diff --git a/grid2op/Environment/_obsEnv.py b/grid2op/Environment/_obsEnv.py index 0c713f707..172235eb7 100644 --- a/grid2op/Environment/_obsEnv.py +++ b/grid2op/Environment/_obsEnv.py @@ -75,6 +75,8 @@ def __init__( highres_sim_counter=None, _complete_action_cls=None, _ptr_orig_obs_space=None, + _local_dir_cls=None, # only set at the first call to `make(...)` after should be false + _read_from_local_dir=None, ): BaseEnv.__init__( self, @@ -92,7 +94,10 @@ def __init__( logger=logger, highres_sim_counter=highres_sim_counter, update_obs_after_reward=False, + _local_dir_cls=_local_dir_cls, + _read_from_local_dir=_read_from_local_dir ) + self._do_not_erase_local_dir_cls = True self.__unusable = False # unsuable if backend cannot be copied self._reward_helper = reward_helper @@ -101,12 +106,13 @@ def __init__( # initialize the observation space self._obsClass = None - + + cls = type(self) # line status (inherited from BaseEnv) - self._line_status = np.full(self.n_line, dtype=dt_bool, fill_value=True) + self._line_status = np.full(cls.n_line, dtype=dt_bool, fill_value=True) # line status (for this usage) self._line_status_me = np.ones( - shape=self.n_line, dtype=dt_int + shape=cls.n_line, dtype=dt_int ) # this is "line status" but encode in +1 / -1 if self._thermal_limit_a is None: @@ -114,6 +120,8 @@ def __init__( else: self._thermal_limit_a[:] = thermal_limit_a + self.current_obs_init = None + self.current_obs = None self._init_backend( chronics_handler=_ObsCH(), backend=backend_instanciated, @@ -128,13 +136,13 @@ def __init__( #### # to be able to save and import (using env.generate_classes) correctly self._actionClass = action_helper.subtype - self._observationClass = _complete_action_cls # not used anyway self._complete_action_cls = _complete_action_cls self._action_space = ( action_helper # obs env and env share the same action space ) self._ptr_orig_obs_space = _ptr_orig_obs_space + #### self.no_overflow_disconnection = parameters.NO_OVERFLOW_DISCONNECTION @@ -178,6 +186,8 @@ def _init_backend( if backend is None: self.__unusable = True return + self._actionClass_orig = actionClass + self._observationClass_orig = observationClass self.__unusable = False self._env_dc = self.parameters.ENV_DC @@ -195,19 +205,22 @@ def _init_backend( from grid2op.Observation import ObservationSpace from grid2op.Reward import FlatReward - ob_sp_cls = ObservationSpace.init_grid(type(backend)) + ob_sp_cls = ObservationSpace.init_grid(type(backend), _local_dir_cls=self._local_dir_cls) self._observation_space = ob_sp_cls(type(backend), env=self, with_forecast=False, rewardClass=FlatReward, - _with_obs_env=False) + _with_obs_env=False, + _local_dir_cls=self._local_dir_cls + ) + self._observationClass = self._observation_space.subtype # not used anyway # create the opponent self._create_opponent() # create the attention budget self._create_attention_budget() - self._obsClass = observationClass.init_grid(type(self.backend)) + self._obsClass = observationClass.init_grid(type(self.backend), _local_dir_cls=self._local_dir_cls) self._obsClass._INIT_GRID_CLS = observationClass self.current_obs_init = self._obsClass(obs_env=None, action_helper=None) self.current_obs = self.current_obs_init @@ -216,7 +229,7 @@ def _init_backend( self._init_alert_data() # backend has loaded everything - self._hazard_duration = np.zeros(shape=self.n_line, dtype=dt_int) + self._hazard_duration = np.zeros(shape=type(self).n_line, dtype=dt_int) def _do_nothing(self, x): """ @@ -247,7 +260,7 @@ def _update_actions(self): # This "environment" doesn't modify anything return self._do_nothing_act, None - def copy(self): + def copy(self, env=None, new_obs_space=None): """ INTERNAL @@ -263,17 +276,44 @@ def copy(self): if self.__unusable: raise EnvError("Impossible to use a Observation backend with an " "environment that cannot be copied.") - backend = self.backend - self.backend = None - _highres_sim_counter = self._highres_sim_counter - self._highres_sim_counter = None - with warnings.catch_warnings(): - warnings.simplefilter("ignore", FutureWarning) - res = copy.deepcopy(self) - res.backend = backend.copy() - res._highres_sim_counter = _highres_sim_counter - self.backend = backend - self._highres_sim_counter = _highres_sim_counter + + my_cls = type(self) + res = my_cls.__new__(my_cls) + + # fill its attribute + res.__unusable = self.__unusable + res._obsClass = self._obsClass + res._line_status = copy.deepcopy(self._line_status) + res._line_status_me = copy.deepcopy(self._line_status_me) + if env is not None: + # res._ptr_orig_obs_space = env._observation_space # this is not created when this function is called + # so this is why i pass the `new_obs_space` as argument + res._ptr_orig_obs_space = new_obs_space + else: + res._ptr_orig_obs_space = self._ptr_orig_obs_space + res.no_overflow_disconnection = self.parameters.NO_OVERFLOW_DISCONNECTION + res._topo_vect = copy.deepcopy(self._topo_vect) + res.is_init = self.is_init + if env is not None: + res._helper_action_env = env._helper_action_env + else: + res._helper_action_env = self._helper_action_env + res._disc_lines = copy.deepcopy(self._disc_lines) + res._highres_sim_counter = self._highres_sim_counter + res._max_episode_duration = self._max_episode_duration + + res.current_obs_init = self._obsClass(obs_env=None, action_helper=None) + res.current_obs_init.reset() + res.current_obs = res.current_obs_init + + # copy attribute of "super" + super()._custom_deepcopy_for_copy(res) + + # finish to initialize res + res.env_modification = res._helper_action_env() + res._do_nothing_act = res._helper_action_env() + res._backend_action_set = res._backend_action_class() + res.current_obs = res.current_obs_init return res def _reset_to_orig_state(self, obs): diff --git a/grid2op/Environment/baseEnv.py b/grid2op/Environment/baseEnv.py index ef1f024b5..440d89e9d 100644 --- a/grid2op/Environment/baseEnv.py +++ b/grid2op/Environment/baseEnv.py @@ -8,13 +8,15 @@ from datetime import datetime -import shutil +import tempfile import logging import time import copy import os import json from typing import Optional, Tuple, Union, Dict, Any, Literal +import importlib +import sys import warnings import numpy as np @@ -307,6 +309,7 @@ def __init__( init_grid_path: os.PathLike, parameters: Parameters, voltagecontrolerClass: type, + name="unknown", thermal_limit_a: Optional[np.ndarray] = None, epsilon_poly: float = 1e-4, # precision of the redispatching algorithm tol_poly: float = 1e-2, # i need to compute a redispatching if the actual values are "more than tol_poly" the values they should be @@ -332,10 +335,30 @@ def __init__( update_obs_after_reward=False, n_busbar=2, _is_test: bool = False, # TODO not implemented !! - _init_obs: Optional[BaseObservation] =None + _init_obs: Optional[BaseObservation] =None, + _local_dir_cls=None, + _read_from_local_dir=None, + _raw_backend_class=None, ): + #: flag to indicate not to erase the directory when the env has been used + self._do_not_erase_local_dir_cls = False GridObjects.__init__(self) RandomObject.__init__(self) + self.name = name + self._local_dir_cls = _local_dir_cls # suppose it's the second path to the environment, so the classes are already in the files + self._read_from_local_dir = _read_from_local_dir + if self._read_from_local_dir is not None: + if os.path.split(self._read_from_local_dir)[1] == "_grid2op_classes": + # legacy behaviour (using experimental_read_from_local_dir kwargs in env.make) + self._do_not_erase_local_dir_cls = True + else: + self._do_not_erase_local_dir_cls = True + + self._actionClass_orig = None + self._observationClass_orig = None + + self._raw_backend_class = _raw_backend_class + self._n_busbar = n_busbar # env attribute not class attribute ! if other_rewards is None: other_rewards = {} @@ -388,10 +411,10 @@ def __init__( # class used for the action spaces self._helper_action_class: ActionSpace = None - self._helper_observation_class: ActionSpace = None + self._helper_observation_class: ObservationSpace = None # and calendar data - self.time_stamp: time.struct_time = None + self.time_stamp: time.struct_time = datetime(year=2019, month=1, day=1) self.nb_time_step: datetime.timedelta = dt_int(0) self.delta_time_seconds = None # number of seconds between two consecutive step @@ -621,17 +644,26 @@ def _custom_deepcopy_for_copy(self, new_obj, dict_=None): if self.__closed: raise RuntimeError("Impossible to make a copy of a closed environment !") - if not self.backend._can_be_copied: - raise RuntimeError("Impossible to copy your environment: the backend " - "class you used cannot be copied.") + if hasattr(self.backend, "_can_be_copied"): + if not self.backend._can_be_copied: + # introduced later on, might not be copied perfectly for some older backends + raise RuntimeError("Impossible to copy your environment: the backend " + "class you used cannot be copied.") + # for earlier backend it is not possible to check this so I ignore it. + RandomObject._custom_deepcopy_for_copy(self, new_obj) + new_obj.name = self.name if dict_ is None: dict_ = {} new_obj._n_busbar = self._n_busbar new_obj._init_grid_path = copy.deepcopy(self._init_grid_path) new_obj._init_env_path = copy.deepcopy(self._init_env_path) + new_obj._local_dir_cls = None # copy of a env is not the "main" env. TODO + new_obj._do_not_erase_local_dir_cls = self._do_not_erase_local_dir_cls + new_obj._read_from_local_dir = self._read_from_local_dir + new_obj._raw_backend_class = self._raw_backend_class new_obj._DEBUG = self._DEBUG new_obj._parameters = copy.deepcopy(self._parameters) new_obj.with_forecast = self.with_forecast @@ -651,27 +683,23 @@ def _custom_deepcopy_for_copy(self, new_obj, dict_=None): new_obj._tol_poly = self._tol_poly # - new_obj._complete_action_cls = copy.deepcopy(self._complete_action_cls) + new_obj._complete_action_cls = self._complete_action_cls # const # define logger new_obj.logger = copy.deepcopy(self.logger) # TODO does that make any sense ? # class used for the action spaces new_obj._helper_action_class = self._helper_action_class # const - new_obj._helper_observation_class = self._helper_observation_class + new_obj._helper_observation_class = self._helper_observation_class # const # and calendar data new_obj.time_stamp = self.time_stamp new_obj.nb_time_step = self.nb_time_step new_obj.delta_time_seconds = self.delta_time_seconds - # observation - if self.current_obs is not None: - new_obj.current_obs = self.current_obs.copy() - # backend # backend action - new_obj._backend_action_class = self._backend_action_class + new_obj._backend_action_class = self._backend_action_class # const new_obj._backend_action = copy.deepcopy(self._backend_action) # specific to Basic Env, do not change @@ -760,25 +788,29 @@ def _custom_deepcopy_for_copy(self, new_obj, dict_=None): new_obj._rewardClass = self._rewardClass new_obj._actionClass = self._actionClass + new_obj._actionClass_orig = self._actionClass_orig new_obj._observationClass = self._observationClass + new_obj._observationClass_orig = self._observationClass_orig new_obj._legalActClass = self._legalActClass - new_obj._observation_space = self._observation_space.copy(copy_backend=True) - new_obj._observation_space._legal_action = ( - new_obj._game_rules.legal_action - ) # TODO this does not respect SOLID principles at all ! - new_obj._kwargs_observation = copy.deepcopy(self._kwargs_observation) - new_obj._observation_space._ptr_kwargs_observation = new_obj._kwargs_observation - new_obj._names_chronics_to_backend = self._names_chronics_to_backend - new_obj._reward_helper = copy.deepcopy(self._reward_helper) - - # gym compatibility - new_obj.reward_range = copy.deepcopy(self.reward_range) - new_obj._viewer = copy.deepcopy(self._viewer) - new_obj.viewer_fig = copy.deepcopy(self.viewer_fig) - + new_obj._names_chronics_to_backend = self._names_chronics_to_backend # cst + # other rewards - new_obj.other_rewards = copy.deepcopy(self.other_rewards) - + new_obj.other_rewards = {k: copy.deepcopy(v) for k, v in self.other_rewards.items()} + for extra_reward in new_obj.other_rewards.values(): + extra_reward.reset(new_obj) + + # voltage + new_obj._voltagecontrolerClass = self._voltagecontrolerClass + if self._voltage_controler is not None: + new_obj._voltage_controler = self._voltage_controler.copy() + else: + new_obj._voltage_controler = None + + # needed for the "Environment.get_kwargs(env, False, False)" (used in the observation_space) + new_obj._attention_budget_cls = self._attention_budget_cls # const + new_obj._kwargs_attention_budget = copy.deepcopy(self._kwargs_attention_budget) + new_obj._has_attention_budget = self._has_attention_budget + # opponent new_obj._opponent_space_type = self._opponent_space_type new_obj._opponent_action_class = self._opponent_action_class # const @@ -795,6 +827,27 @@ def _custom_deepcopy_for_copy(self, new_obj, dict_=None): new_obj._compute_opp_budget = self._opponent_budget_class( self._opponent_action_space ) + + new_obj._observation_bk_class = self._observation_bk_class + new_obj._observation_bk_kwargs = self._observation_bk_kwargs + + # do not copy it. + new_obj._highres_sim_counter = self._highres_sim_counter + + # observation space (might depends on the previous things) + # at this stage the function "Environment.get_kwargs(env, False, False)" should run + new_obj._kwargs_observation = copy.deepcopy(self._kwargs_observation) + new_obj._observation_space = self._observation_space.copy(copy_backend=True, env=new_obj) + new_obj._observation_space._legal_action = ( + new_obj._game_rules.legal_action + ) # TODO this does not respect SOLID principles at all ! + new_obj._observation_space._ptr_kwargs_observation = new_obj._kwargs_observation + new_obj._reward_helper = copy.deepcopy(self._reward_helper) + + # gym compatibility + new_obj.reward_range = copy.deepcopy(self.reward_range) + new_obj._viewer = copy.deepcopy(self._viewer) + new_obj.viewer_fig = copy.deepcopy(self.viewer_fig) # init the opponent new_obj._opponent = new_obj._opponent_class.__new__(new_obj._opponent_class) @@ -808,15 +861,11 @@ def _custom_deepcopy_for_copy(self, new_obj, dict_=None): attack_duration=new_obj._opponent_attack_duration, attack_cooldown=new_obj._opponent_attack_cooldown, budget_per_timestep=new_obj._opponent_budget_per_ts, - opponent=new_obj._opponent, + opponent=new_obj._opponent ) state_me, state_opp = self._oppSpace._get_state() new_obj._oppSpace._set_state(state_me) - - # voltage - new_obj._voltagecontrolerClass = self._voltagecontrolerClass - new_obj._voltage_controler = self._voltage_controler.copy() - + # to change the parameters new_obj.__new_param = copy.deepcopy(self.__new_param) new_obj.__new_forecast_param = copy.deepcopy(self.__new_forecast_param) @@ -840,19 +889,13 @@ def _custom_deepcopy_for_copy(self, new_obj, dict_=None): new_obj._limited_before = copy.deepcopy(self._limited_before) # attention budget - new_obj._has_attention_budget = self._has_attention_budget new_obj._attention_budget = copy.deepcopy(self._attention_budget) - new_obj._attention_budget_cls = self._attention_budget_cls # const new_obj._is_alarm_illegal = copy.deepcopy(self._is_alarm_illegal) new_obj._is_alarm_used_in_reward = copy.deepcopy(self._is_alarm_used_in_reward) # alert new_obj._is_alert_illegal = copy.deepcopy(self._is_alert_illegal) new_obj._is_alert_used_in_reward = copy.deepcopy(self._is_alert_used_in_reward) - - new_obj._kwargs_attention_budget = copy.deepcopy(self._kwargs_attention_budget) - - new_obj._last_obs = self._last_obs.copy() new_obj._has_just_been_seeded = self._has_just_been_seeded @@ -865,14 +908,8 @@ def _custom_deepcopy_for_copy(self, new_obj, dict_=None): else: new_obj._init_obs = self._init_obs.copy() - new_obj._observation_bk_class = self._observation_bk_class - new_obj._observation_bk_kwargs = self._observation_bk_kwargs - # do not forget ! - new_obj._is_test = self._is_test - - # do not copy it. - new_obj._highres_sim_counter = self._highres_sim_counter + new_obj._is_test = self._is_test # alert new_obj._last_alert = copy.deepcopy(self._last_alert) @@ -886,6 +923,17 @@ def _custom_deepcopy_for_copy(self, new_obj, dict_=None): new_obj._update_obs_after_reward = copy.deepcopy(self._update_obs_after_reward) + if self._last_obs is not None: + new_obj._last_obs = self._last_obs.copy(env=new_obj) + else: + new_obj._last_obs = None + + # observation + from grid2op.Environment._obsEnv import _ObsEnv + if self.current_obs is not None and not isinstance(self, _ObsEnv): + # breaks for some version of lightsim2grid... (a powerflow need to be run to retrieve the observation) + new_obj.current_obs = new_obj.get_obs() + def get_path_env(self): """ Get the path that allows to create this environment. @@ -1226,6 +1274,7 @@ def _create_opponent(self): gridobj=type(self.backend), legal_action=AlwaysLegal, actionClass=self._opponent_action_class, + _local_dir_cls=self._local_dir_cls ) self._compute_opp_budget = self._opponent_budget_class( @@ -1239,6 +1288,7 @@ def _create_opponent(self): attack_cooldown=self._opponent_attack_cooldown, budget_per_timestep=self._opponent_budget_per_ts, opponent=self._opponent, + _local_dir_cls=self._local_dir_cls, ) self._oppSpace.init_opponent(partial_env=self, **self._kwargs_opponent) self._oppSpace.reset() @@ -1248,13 +1298,19 @@ def _init_myclass(self): # the class has already been initialized return # remember the original grid2op class - type(self)._INIT_GRID_CLS = type(self) + orig_cls = type(self) - bk_type = type( - self.backend - ) # be careful here: you need to initialize from the class, and not from the object + # be careful here: you need to initialize from the class, and not from the object + bk_type = type(self.backend) # create the proper environment class for this specific environment - self.__class__ = type(self).init_grid(bk_type) + new_cls = type(self).init_grid(bk_type, _local_dir_cls=self._local_dir_cls) + # assign the right initial grid class + if orig_cls._INIT_GRID_CLS is None: + new_cls._INIT_GRID_CLS = orig_cls + else: + new_cls._INIT_GRID_CLS = orig_cls._INIT_GRID_CLS + + self.__class__ = new_cls def _has_been_initialized(self): # type of power flow to play @@ -1263,7 +1319,7 @@ def _has_been_initialized(self): bk_type = type(self.backend) if np.min([self.n_line, self.n_gen, self.n_load, self.n_sub]) <= 0: raise EnvironmentError("Environment has not been initialized properly") - self._backend_action_class = _BackendAction.init_grid(bk_type) + self._backend_action_class = _BackendAction.init_grid(bk_type, _local_dir_cls=self._local_dir_cls) self._backend_action = self._backend_action_class() # initialize maintenance / hazards @@ -3693,7 +3749,24 @@ def close(self): if hasattr(self, attr_nm): delattr(self, attr_nm) setattr(self, attr_nm, None) - + + if self._do_not_erase_local_dir_cls: + # The resources are not held by this env, so + # I do not remove them + # (case for ObsEnv or ForecastedEnv) + return + self._aux_close_local_dir_cls() + + def _aux_close_local_dir_cls(self): + if self._local_dir_cls is not None: + # I am the "keeper" of the temporary directory + # deleting this env should also delete the temporary directory + if not (hasattr(self._local_dir_cls, "_RUNNER_DO_NOT_ERASE") and not self._local_dir_cls._RUNNER_DO_NOT_ERASE): + # BUT if a runner uses it, then I should not delete it ! + self._local_dir_cls.cleanup() + self._local_dir_cls = None + # In this case it's likely that the OS will clean it for grid2op with a warning... + def attach_layout(self, grid_layout): """ Compare to the method of the base class, this one performs a check. @@ -3957,30 +4030,84 @@ def change_reward(self, new_reward_func): ) self.__new_reward_func = new_reward_func - def _aux_gen_classes(self, cls, sys_path): - if not isinstance(cls, type): - raise RuntimeError(f"cls should be a type and not an object !: {cls}") - if not issubclass(cls, GridObjects): - raise RuntimeError(f"cls should inherit from GridObjects: {cls}") + @staticmethod + def _aux_gen_classes(cls_other, sys_path, _add_class_output=False): + if not isinstance(cls_other, type): + raise RuntimeError(f"cls_other should be a type and not an object !: {cls_other}") + if not issubclass(cls_other, GridObjects): + raise RuntimeError(f"cls_other should inherit from GridObjects: {cls_other}") from pathlib import Path - path_env = cls._PATH_GRID_CLASSES - cls._PATH_GRID_CLASSES = str(Path(self.get_path_env()).as_posix()) + path_env = cls_other._PATH_GRID_CLASSES + # cls_other._PATH_GRID_CLASSES = str(Path(self.get_path_env()).as_posix()) + cls_other._PATH_GRID_CLASSES = str(Path(sys_path).as_posix()) - res = cls._get_full_cls_str() - cls._PATH_GRID_CLASSES = path_env - output_file = os.path.join(sys_path, f"{cls.__name__}_file.py") + res = cls_other._get_full_cls_str() + cls_other._PATH_GRID_CLASSES = path_env + output_file = os.path.join(sys_path, f"{cls_other.__name__}_file.py") if not os.path.exists(output_file): # if the file is not already saved, i save it and add it to the __init__ file with open(output_file, "w", encoding="utf-8") as f: f.write(res) - return f"\nfrom .{cls.__name__}_file import {cls.__name__}" + str_import = f"\nfrom .{cls_other.__name__}_file import {cls_other.__name__}" else: # if the file exists, I check it's the same - # from grid2op.MakeEnv.UpdateEnv import _aux_hash_file, _aux_update_hash_text - # hash_saved = _aux_hash_file(output_file) - # my_hash = _aux_update_hash_text(res) - return "" + from grid2op.MakeEnv.UpdateEnv import _aux_hash_file, _aux_update_hash_text + hash_saved = _aux_hash_file(output_file) + my_hash = _aux_update_hash_text(res) + if hash_saved.hexdigest() != my_hash.hexdigest(): + raise EnvError(f"It appears some classes have been modified between what was saved on the hard drive " + f"and the current state of the grid. This should not have happened. " + f"Check class {cls_other.__name__}") + str_import = None + if not _add_class_output: + return str_import + + # NB: these imports needs to be consistent with what is done in + # griobj.init_grid(...) + package_path, nm_ = os.path.split(output_file) + nm_, ext = os.path.splitext(nm_) + sub_repo, tmp_nm = os.path.split(package_path) + if sub_repo not in sys.path: + sys.path.append(sub_repo) + + sub_repo_mod = None + if tmp_nm == "_grid2op_classes": + # legacy "experimental_read_from_local_dir" + # issue was the module "_grid2op_classes" had the same name + # regardless of the environment, so grid2op was "confused" + path_init = os.path.join(sub_repo, "__init__.py") + if not os.path.exists(path_init): + try: + with open(path_init, "w", encoding='utf-8') as f: + f.write("# DO NOT REMOVE, automatically generated by grid2op") + except FileExistsError: + pass + env_path, env_nm = os.path.split(sub_repo) + if env_path not in sys.path: + sys.path.append(env_path) + if not package_path in sys.path: + sys.path.append(package_path) + super_supermodule = importlib.import_module(env_nm) + nm_ = f"{tmp_nm}.{nm_}" + tmp_nm = env_nm + super_module = importlib.import_module(tmp_nm, package=sub_repo_mod) + add_sys_path = os.path.dirname(super_module.__file__) + if not add_sys_path in sys.path: + sys.path.append(add_sys_path) + + if f"{tmp_nm}.{nm_}" in sys.modules: + cls_res = getattr(sys.modules[f"{tmp_nm}.{nm_}"], cls_other.__name__) + return str_import, cls_res + try: + module = importlib.import_module(f".{nm_}", package=tmp_nm) + except ModuleNotFoundError as exc_: + # invalidate the cache and reload the package in this case + importlib.invalidate_caches() + importlib.reload(super_module) + module = importlib.import_module(f".{nm_}", package=tmp_nm) + cls_res = getattr(module, cls_other.__name__) + return str_import, cls_res def generate_classes(self, *, local_dir_id=None, _guard=None, _is_base_env__=True, sys_path=None): """ @@ -4050,7 +4177,6 @@ def generate_classes(self, *, local_dir_id=None, _guard=None, _is_base_env__=Tru if self.__closed: return - # create the folder if _guard is not None: raise RuntimeError("use `env.generate_classes()` with no arguments !") @@ -4071,42 +4197,79 @@ def generate_classes(self, *, local_dir_id=None, _guard=None, _is_base_env__=Tru sys_path = os.path.join(self.get_path_env(), "_grid2op_classes", local_dir_id) else: sys_path = os.path.join(self.get_path_env(), "_grid2op_classes") - + if _is_base_env__: if os.path.exists(sys_path): shutil.rmtree(sys_path) os.mkdir(sys_path) + + with open(os.path.join(sys_path, "__init__.py"), "w", encoding="utf-8") as f: + f.write(BASE_TXT_COPYRIGHT) # initialized the "__init__" file _init_txt = "" - mode = "w" - if not _is_base_env__: - _init_txt = BASE_TXT_COPYRIGHT + _init_txt - else: - # i am apppending to the __init__ file in case of obs_env - mode = "a" + mode = "a" # generate the classes - _init_txt += self._aux_gen_classes(type(self), sys_path) - _init_txt += self._aux_gen_classes(type(self.backend), sys_path) - _init_txt += self._aux_gen_classes( - self.backend._complete_action_class, sys_path + # for the environment + txt_ = self._aux_gen_classes(type(self), sys_path) + if txt_ is not None: + _init_txt += txt_ + + # for the forecast env (we do this even if it's not used) + from grid2op.Environment._forecast_env import _ForecastEnv + for_env_cls = _ForecastEnv.init_grid(type(self.backend), _local_dir_cls=self._local_dir_cls) + txt_ = self._aux_gen_classes(for_env_cls, sys_path, _add_class_output=False) + if txt_ is not None: + _init_txt += txt_ + + # for the backend + txt_, cls_res_bk = self._aux_gen_classes(type(self.backend), sys_path, _add_class_output=True) + if txt_ is not None: + _init_txt += txt_ + old_bk_cls = self.backend.__class__ + self.backend.__class__ = cls_res_bk + txt_, cls_res_complete_act = self._aux_gen_classes( + old_bk_cls._complete_action_class, sys_path, _add_class_output=True ) - _init_txt += self._aux_gen_classes(self._backend_action_class, sys_path) - _init_txt += self._aux_gen_classes(type(self.action_space), sys_path) - _init_txt += self._aux_gen_classes(self._actionClass, sys_path) - _init_txt += self._aux_gen_classes(self._complete_action_cls, sys_path) - _init_txt += self._aux_gen_classes(type(self.observation_space), sys_path) - _init_txt += self._aux_gen_classes(self._observationClass, sys_path) - _init_txt += self._aux_gen_classes( + if txt_ is not None: + _init_txt += txt_ + self.backend.__class__._complete_action_class = cls_res_complete_act + txt_, cls_res_bk_act = self._aux_gen_classes(self._backend_action_class, sys_path, _add_class_output=True) + if txt_ is not None: + _init_txt += txt_ + self._backend_action_class = cls_res_bk_act + self.backend.__class__.my_bk_act_class = cls_res_bk_act + + # for the other class + txt_ = self._aux_gen_classes(type(self.action_space), sys_path) + if txt_ is not None: + _init_txt += txt_ + txt_ = self._aux_gen_classes(self._actionClass, sys_path) + if txt_ is not None: + _init_txt += txt_ + txt_ = self._aux_gen_classes(self._complete_action_cls, sys_path) + if txt_ is not None: + _init_txt += txt_ + txt_ = self._aux_gen_classes(type(self.observation_space), sys_path) + if txt_ is not None: + _init_txt += txt_ + txt_ = self._aux_gen_classes(self._observationClass, sys_path) + if txt_ is not None: + _init_txt += txt_ + txt_ = self._aux_gen_classes( self._opponent_action_space.subtype, sys_path ) + if txt_ is not None: + _init_txt += txt_ # now do the same for the obs_env if _is_base_env__: - _init_txt += self._aux_gen_classes( + txt_ = self._aux_gen_classes( self._voltage_controler.action_space.subtype, sys_path ) + if txt_ is not None: + _init_txt += txt_ init_grid_tmp = self._observation_space.obs_env._init_grid_path self._observation_space.obs_env._init_grid_path = self._init_grid_path @@ -4119,50 +4282,6 @@ def generate_classes(self, *, local_dir_id=None, _guard=None, _is_base_env__=Tru _init_txt += "\n" with open(os.path.join(sys_path, "__init__.py"), mode, encoding="utf-8") as f: f.write(_init_txt) - - def _forget_classes(self): - """ - This function allows python to "forget" the classes created at the initialization of the environment. - - It should not be used in most cases and is reserved for internal use only. - - .. versionadded: 1.10.2 - Function added following the new behaviour introduced in this version. - - """ - from grid2op.MakeEnv.PathUtils import USE_CLASS_IN_FILE - if not USE_CLASS_IN_FILE: - return - pass - - def remove_all_class_folders(self): - """ - This function allows python to remove all the files containing all the classes - in the environment. - - .. warning:: - If you have pending grid2op "job" using this environment, they will most likely crash - so use with extra care ! - - It should not be used in most cases and is reserved for internal use only. - - .. versionadded: 1.10.2 - Function added following the new behaviour introduced in this version. - - """ - directory_path = os.path.join(self.get_path_env(), "_grid2op_classes") - try: - with os.scandir(directory_path) as entries: - for entry in entries: - try: - if entry.is_file(): - os.unlink(entry.path) - else: - shutil.rmtree(entry.path) - except (OSError, FileNotFoundError): - pass - except OSError: - pass def __del__(self): """when the environment is garbage collected, free all the memory, including cross reference to itself in the observation space.""" @@ -4321,4 +4440,17 @@ def _check_rules_correct(legalActClass): 'grid2op.BaseRules class, type provided is "{}"'.format( type(legalActClass) ) - ) \ No newline at end of file + ) + + def classes_are_in_files(self) -> bool: + """ + + Whether the classes created when this environment has been made are + store on the hard drive (will return `True`) or not. + + .. info:: + This will become the default behaviour in future grid2op versions. + + See :ref:`troubleshoot_pickle` for more information. + """ + return self._read_from_local_dir is not None diff --git a/grid2op/Environment/baseMultiProcessEnv.py b/grid2op/Environment/baseMultiProcessEnv.py index 0f76ca9d9..b2e7aecdc 100644 --- a/grid2op/Environment/baseMultiProcessEnv.py +++ b/grid2op/Environment/baseMultiProcessEnv.py @@ -325,6 +325,7 @@ def __init__(self, envs, obs_as_class=True, return_info=True, logger=None): self.obs_as_class = obs_as_class # self.__return_info = return_info self._waiting = True + self._read_from_local_dir = env._read_from_local_dir def _send_act(self, actions): for remote, action in zip(self._remotes, actions): diff --git a/grid2op/Environment/environment.py b/grid2op/Environment/environment.py index 8e7214d05..97b5d0a2e 100644 --- a/grid2op/Environment/environment.py +++ b/grid2op/Environment/environment.py @@ -34,6 +34,7 @@ from grid2op.operator_attention import LinearAttentionBudget from grid2op.Space import DEFAULT_N_BUSBAR_PER_SUB from grid2op.typing_variables import RESET_OPTIONS_TYPING, N_BUSBAR_PER_SUB_TYPING +from grid2op.MakeEnv.PathUtils import USE_CLASS_IN_FILE class Environment(BaseEnv): @@ -117,9 +118,11 @@ def __init__( _init_obs=None, _raw_backend_class=None, _compat_glop_version=None, - _read_from_local_dir=True, + _read_from_local_dir=None, _is_test=False, _allow_loaded_backend=False, + _local_dir_cls=None, # only set at the first call to `make(...)` after should be false + _overload_name_multimix=None, ): BaseEnv.__init__( self, @@ -153,17 +156,31 @@ def __init__( highres_sim_counter=highres_sim_counter, update_obs_after_reward=_update_obs_after_reward, n_busbar=n_busbar, # TODO n_busbar_per_sub different num per substations: read from a config file maybe (if not provided by the user) + name=name, + _raw_backend_class=_raw_backend_class if _raw_backend_class is not None else type(backend), _init_obs=_init_obs, _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, ) + if name == "unknown": warnings.warn( 'It is NOT recommended to create an environment without "make" and EVEN LESS ' "to use an environment without a name..." ) - self.name = name - self._read_from_local_dir = _read_from_local_dir - + + if _overload_name_multimix is not None: + # this means that the "make" call is issued from the + # creation of a MultiMix. + # So I use the base name instead. + self.name = "".join(_overload_name_multimix[2:]) + self.multimix_mix_name = name + self._overload_name_multimix = _overload_name_multimix + else: + self.name = name + self._overload_name_multimix = None + self.multimix_mix_name = None # to remember if the user specified a "max_iter" at some point self._max_iter = chronics_handler.max_iter # for all episode, set in the chronics_handler or by a call to `env.set_max_iter` self._max_step = None # for the current episode @@ -178,13 +195,11 @@ def __init__( self.metadata = None self.spec = None - if _raw_backend_class is None: - self._raw_backend_class = type(backend) - else: - self._raw_backend_class = _raw_backend_class - self._compat_glop_version = _compat_glop_version + # needs to be done before "_init_backend" otherwise observationClass is not defined in the + # observation space (real_env_kwargs) + self._observationClass_orig = observationClass # for plotting self._init_backend( chronics_handler, @@ -195,8 +210,6 @@ def __init__( rewardClass, legalActClass, ) - self._actionClass_orig = actionClass - self._observationClass_orig = observationClass def _init_backend( self, @@ -244,8 +257,9 @@ def _init_backend( "Impossible to use the same backend twice. Please create your environment with a " "new backend instance (new object)." ) - - need_process_backend = False + self._actionClass_orig = actionClass + + need_process_backend = False if not self.backend.is_loaded: if hasattr(self.backend, "init_pp_backend") and self.backend.init_pp_backend is not None: # hack for lightsim2grid ... @@ -258,7 +272,8 @@ def _init_backend( # example if self._read_from_local_dir is not None: # test to support pickle conveniently - self.backend._PATH_GRID_CLASSES = self.get_path_env() + # type(self.backend)._PATH_GRID_CLASSES = self.get_path_env() + self.backend._PATH_GRID_CLASSES = self._read_from_local_dir # all the above should be done in this exact order, otherwise some weird behaviour might occur # this is due to the class attribute type(self.backend).set_env_name(self.name) @@ -289,7 +304,8 @@ def _init_backend( self.load_alert_data() # to force the initialization of the backend to the proper type - self.backend.assert_grid_correct() + self.backend.assert_grid_correct( + _local_dir_cls=self._local_dir_cls) self.backend.is_loaded = True need_process_backend = True @@ -345,24 +361,26 @@ def _init_backend( # be careful here: you need to initialize from the class, and not from the object bk_type = type(self.backend) self._rewardClass = rewardClass - self._actionClass = actionClass.init_grid(gridobj=bk_type) + self._actionClass = actionClass.init_grid(gridobj=bk_type, _local_dir_cls=self._local_dir_cls) self._actionClass._add_shunt_data() self._actionClass._update_value_set() - self._observationClass = observationClass.init_grid(gridobj=bk_type) + self._observationClass = observationClass.init_grid(gridobj=bk_type, _local_dir_cls=self._local_dir_cls) - self._complete_action_cls = CompleteAction.init_grid(gridobj=bk_type) + self._complete_action_cls = CompleteAction.init_grid(gridobj=bk_type, _local_dir_cls=self._local_dir_cls) - self._helper_action_class = ActionSpace.init_grid(gridobj=bk_type) + self._helper_action_class = ActionSpace.init_grid(gridobj=bk_type, _local_dir_cls=self._local_dir_cls) self._action_space = self._helper_action_class( gridobj=bk_type, actionClass=actionClass, legal_action=self._game_rules.legal_action, + _local_dir_cls=self._local_dir_cls ) # action that affect the grid made by the environment. self._helper_action_env = self._helper_action_class( gridobj=bk_type, actionClass=CompleteAction, legal_action=self._game_rules.legal_action, + _local_dir_cls=self._local_dir_cls, ) # handles input data @@ -391,7 +409,7 @@ def _init_backend( # this needs to be done after the chronics handler: rewards might need information # about the chronics to work properly. - self._helper_observation_class = ObservationSpace.init_grid(gridobj=bk_type) + self._helper_observation_class = ObservationSpace.init_grid(gridobj=bk_type, _local_dir_cls=self._local_dir_cls) # FYI: this try to copy the backend if it fails it will modify the backend # and the environment to force the deactivation of the # forecasts @@ -403,7 +421,8 @@ def _init_backend( env=self, kwargs_observation=self._kwargs_observation, observation_bk_class=self._observation_bk_class, - observation_bk_kwargs=self._observation_bk_kwargs + observation_bk_kwargs=self._observation_bk_kwargs, + _local_dir_cls=self._local_dir_cls ) # test to make sure the backend is consistent with the chronics generator @@ -426,6 +445,7 @@ def _init_backend( gridobj=bk_type, controler_backend=self.backend, actionSpace_cls=self._helper_action_class, + _local_dir_cls=self._local_dir_cls ) # create the opponent @@ -445,6 +465,9 @@ def _init_backend( self._reward_to_obs = {} do_nothing = self._helper_action_env({}) + # needs to be done at the end, but before the first "step" is called + self._observation_space.set_real_env_kwargs(self) + # see issue https://github.com/rte-france/Grid2Op/issues/617 # thermal limits are set AFTER this initial step _no_overflow_disconnection = self._no_overflow_disconnection @@ -492,7 +515,7 @@ def _init_backend( # reset everything to be consistent self._reset_vectors_and_timings() - + def max_episode_duration(self): """ Return the maximum duration (in number of steps) of the current episode. @@ -915,9 +938,9 @@ def reset_grid(self, """ self.backend.reset( - self._init_grid_path + self._init_grid_path, ) # the real powergrid of the environment - self.backend.assert_grid_correct() + # self.backend.assert_grid_correct() if self._thermal_limit_a is not None: self.backend.set_thermal_limit(self._thermal_limit_a.astype(dt_float)) @@ -1392,19 +1415,15 @@ def render(self, mode="rgb_array"): return rgb_array def _custom_deepcopy_for_copy(self, new_obj): - super()._custom_deepcopy_for_copy(new_obj) - - new_obj.name = self.name - new_obj._read_from_local_dir = self._read_from_local_dir new_obj.metadata = copy.deepcopy(self.metadata) new_obj.spec = copy.deepcopy(self.spec) - new_obj._raw_backend_class = self._raw_backend_class new_obj._compat_glop_version = self._compat_glop_version - new_obj._actionClass_orig = self._actionClass_orig - new_obj._observationClass_orig = self._observationClass_orig new_obj._max_iter = self._max_iter new_obj._max_step = self._max_step + new_obj._overload_name_multimix = self._overload_name_multimix + new_obj.multimix_mix_name = self.multimix_mix_name + super()._custom_deepcopy_for_copy(new_obj) def copy(self) -> "Environment": """ @@ -2098,6 +2117,7 @@ def get_params_for_runner(self): res["envClass"] = Environment # TODO ! res["gridStateclass"] = self.chronics_handler.chronicsClass res["backendClass"] = self._raw_backend_class + res["_overload_name_multimix"] = self._overload_name_multimix if hasattr(self.backend, "_my_kwargs"): res["backend_kwargs"] = self.backend._my_kwargs else: @@ -2137,6 +2157,7 @@ def get_params_for_runner(self): res["kwargs_attention_budget"] = copy.deepcopy(self._kwargs_attention_budget) res["has_attention_budget"] = self._has_attention_budget res["_read_from_local_dir"] = self._read_from_local_dir + res["_local_dir_cls"] = self._local_dir_cls # should be transfered to the runner so that folder is not deleted while runner exists res["logger"] = self.logger res["kwargs_observation"] = copy.deepcopy(self._kwargs_observation) res["observation_bk_class"] = self._observation_bk_class @@ -2180,7 +2201,10 @@ def init_obj_from_kwargs(cls, observation_bk_kwargs, _raw_backend_class, _read_from_local_dir, - n_busbar=DEFAULT_N_BUSBAR_PER_SUB): + _local_dir_cls, + _overload_name_multimix, + n_busbar=DEFAULT_N_BUSBAR_PER_SUB + ): res = cls(init_env_path=init_env_path, init_grid_path=init_grid_path, chronics_handler=chronics_handler, @@ -2213,7 +2237,9 @@ def init_obj_from_kwargs(cls, observation_bk_kwargs=observation_bk_kwargs, n_busbar=int(n_busbar), _raw_backend_class=_raw_backend_class, - _read_from_local_dir=_read_from_local_dir) + _read_from_local_dir=_read_from_local_dir, + _local_dir_cls=_local_dir_cls, + _overload_name_multimix=_overload_name_multimix) return res def generate_data(self, nb_year=1, nb_core=1, seed=None, **kwargs): @@ -2289,3 +2315,20 @@ def generate_data(self, nb_year=1, nb_core=1, seed=None, **kwargs): env=self, seed=seed, nb_scenario=nb_year, nb_core=nb_core, **kwargs ) + + def _add_classes_in_files(self, sys_path, bk_type, are_classes_in_files): + if are_classes_in_files: + # then generate the proper classes + _PATH_GRID_CLASSES = bk_type._PATH_GRID_CLASSES + try: + bk_type._PATH_GRID_CLASSES = None + my_type_tmp = type(self).init_grid(gridobj=bk_type, _local_dir_cls=None) + txt_, cls_res_me = self._aux_gen_classes(my_type_tmp, + sys_path, + _add_class_output=True) + # then add the class to the init file + with open(os.path.join(sys_path, "__init__.py"), "a", encoding="utf-8") as f: + f.write(txt_) + finally: + # make sure to put back the correct _PATH_GRID_CLASSES + bk_type._PATH_GRID_CLASSES = _PATH_GRID_CLASSES diff --git a/grid2op/Environment/maskedEnvironment.py b/grid2op/Environment/maskedEnvironment.py index bd7caaffa..e3c55a7d9 100644 --- a/grid2op/Environment/maskedEnvironment.py +++ b/grid2op/Environment/maskedEnvironment.py @@ -8,14 +8,17 @@ import copy import numpy as np +import os from typing import Tuple, Union, List + from grid2op.Environment.environment import Environment from grid2op.Exceptions import EnvError from grid2op.dtypes import dt_bool, dt_float, dt_int from grid2op.Space import DEFAULT_N_BUSBAR_PER_SUB +from grid2op.MakeEnv.PathUtils import USE_CLASS_IN_FILE -class MaskedEnvironment(Environment): # TODO heritage ou alors on met un truc de base +class MaskedEnvironment(Environment): """This class is the grid2op implementation of a "maked" environment: lines not in the `lines_of_interest` mask will NOT be deactivated by the environment is the flow is too high (or moderately high for too long.) @@ -25,6 +28,29 @@ class MaskedEnvironment(Environment): # TODO heritage ou alors on met un truc d .. warning:: At time of writing, the behaviour of "obs.simulate" is not modified + + Examples + --------- + + We recommend you build such an environment with: + + .. code-block:: python + + import grid2op + from grid2op.Environment import MaskedEnvironment + + env_name = "l2rpn_case14_sandbox" + lines_of_interest = np.array([True, True, True, True, True, True, + False, False, False, False, False, False, + False, False, False, False, False, False, + False, False]) + env = MaskedEnvironment(grid2op.make(env_name), + lines_of_interest=lines_of_interest) + + + In particular, make sure to use `grid2op.make(...)` when creating the MaskedEnvironment + and not to use another environment. + """ # some kind of infinity value # NB we multiply np.finfo(dt_float).max by a small number (1e-7) to avoid overflow @@ -40,20 +66,32 @@ def __init__(self, self._lines_of_interest = self._make_lines_of_interest(lines_of_interest) if isinstance(grid2op_env, Environment): - super().__init__(**grid2op_env.get_kwargs()) + kwargs = grid2op_env.get_kwargs() + if grid2op_env.classes_are_in_files(): + # I need to build the classes + + # first take the "ownership" of the tmp directory + kwargs["_local_dir_cls"] = grid2op_env._local_dir_cls + grid2op_env._local_dir_cls = None + + # then generate the proper classes + sys_path = os.path.abspath(kwargs["_local_dir_cls"].name) + bk_type = type(grid2op_env.backend) + self._add_classes_in_files(sys_path, bk_type, grid2op_env.classes_are_in_files()) + super().__init__(**kwargs) elif isinstance(grid2op_env, dict): super().__init__(**grid2op_env) else: raise EnvError(f"For MaskedEnvironment you need to provide " f"either an Environment or a dict " f"for grid2op_env. You provided: {type(grid2op_env)}") + # if self._lines_of_interest.size() != type(self).n_line: + # raise EnvError("Impossible to init A masked environment when the number of lines " + # "of the mask do not match the number of lines on the grid.") def _make_lines_of_interest(self, lines_of_interest): # NB is called BEFORE the env has been created... if isinstance(lines_of_interest, np.ndarray): - # if lines_of_interest.size() != type(self).n_line: - # raise EnvError("Impossible to init A masked environment when the number of lines " - # "of the mask do not match the number of lines on the grid.") res = lines_of_interest.astype(dt_bool) if res.sum() == 0: raise EnvError("You cannot use MaskedEnvironment and masking all " @@ -89,6 +127,7 @@ def _custom_deepcopy_for_copy(self, new_obj): @classmethod def init_obj_from_kwargs(cls, + *, other_env_kwargs, init_env_path, init_grid_path, @@ -122,39 +161,49 @@ def init_obj_from_kwargs(cls, observation_bk_kwargs, _raw_backend_class, _read_from_local_dir, + _overload_name_multimix, + _local_dir_cls, n_busbar=DEFAULT_N_BUSBAR_PER_SUB): - res = MaskedEnvironment(grid2op_env={"init_env_path": init_env_path, - "init_grid_path": init_grid_path, - "chronics_handler": chronics_handler, - "backend": backend, - "parameters": parameters, - "name": name, - "names_chronics_to_backend": names_chronics_to_backend, - "actionClass": actionClass, - "observationClass": observationClass, - "rewardClass": rewardClass, - "legalActClass": legalActClass, - "voltagecontrolerClass": voltagecontrolerClass, - "other_rewards": other_rewards, - "opponent_space_type": opponent_space_type, - "opponent_action_class": opponent_action_class, - "opponent_class": opponent_class, - "opponent_init_budget": opponent_init_budget, - "opponent_budget_per_ts": opponent_budget_per_ts, - "opponent_budget_class": opponent_budget_class, - "opponent_attack_duration": opponent_attack_duration, - "opponent_attack_cooldown": opponent_attack_cooldown, - "kwargs_opponent": kwargs_opponent, - "with_forecast": with_forecast, - "attention_budget_cls": attention_budget_cls, - "kwargs_attention_budget": kwargs_attention_budget, - "has_attention_budget": has_attention_budget, - "logger": logger, - "kwargs_observation": kwargs_observation, - "observation_bk_class": observation_bk_class, - "observation_bk_kwargs": observation_bk_kwargs, - "n_busbar": int(n_busbar), - "_raw_backend_class": _raw_backend_class, - "_read_from_local_dir": _read_from_local_dir}, - **other_env_kwargs) + grid2op_env = {"init_env_path": init_env_path, + "init_grid_path": init_grid_path, + "chronics_handler": chronics_handler, + "backend": backend, + "parameters": parameters, + "name": name, + "names_chronics_to_backend": names_chronics_to_backend, + "actionClass": actionClass, + "observationClass": observationClass, + "rewardClass": rewardClass, + "legalActClass": legalActClass, + "voltagecontrolerClass": voltagecontrolerClass, + "other_rewards": other_rewards, + "opponent_space_type": opponent_space_type, + "opponent_action_class": opponent_action_class, + "opponent_class": opponent_class, + "opponent_init_budget": opponent_init_budget, + "opponent_budget_per_ts": opponent_budget_per_ts, + "opponent_budget_class": opponent_budget_class, + "opponent_attack_duration": opponent_attack_duration, + "opponent_attack_cooldown": opponent_attack_cooldown, + "kwargs_opponent": kwargs_opponent, + "with_forecast": with_forecast, + "attention_budget_cls": attention_budget_cls, + "kwargs_attention_budget": kwargs_attention_budget, + "has_attention_budget": has_attention_budget, + "logger": logger, + "kwargs_observation": kwargs_observation, + "observation_bk_class": observation_bk_class, + "observation_bk_kwargs": observation_bk_kwargs, + "n_busbar": int(n_busbar), + "_raw_backend_class": _raw_backend_class, + "_read_from_local_dir": _read_from_local_dir, + "_local_dir_cls": _local_dir_cls, + "_overload_name_multimix": _overload_name_multimix} + if not "lines_of_interest" in other_env_kwargs: + raise EnvError("You cannot make a MaskedEnvironment without providing the list of lines of interest") + for el in other_env_kwargs: + if el == "lines_of_interest": + continue + warnings.warn(f"kwargs {el} provided to make the environment will be ignored") + res = MaskedEnvironment(grid2op_env, lines_of_interest=other_env_kwargs["lines_of_interest"]) return res diff --git a/grid2op/Environment/multiMixEnv.py b/grid2op/Environment/multiMixEnv.py index e6ba1a646..be2508478 100644 --- a/grid2op/Environment/multiMixEnv.py +++ b/grid2op/Environment/multiMixEnv.py @@ -16,6 +16,9 @@ from grid2op.Space import GridObjects, RandomObject, DEFAULT_N_BUSBAR_PER_SUB from grid2op.Exceptions import EnvError, Grid2OpException from grid2op.Observation import BaseObservation +from grid2op.MakeEnv.PathUtils import USE_CLASS_IN_FILE +from grid2op.Environment.baseEnv import BaseEnv +from grid2op.typing_variables import STEP_INFO_TYPING, RESET_OPTIONS_TYPING class MultiMixEnvironment(GridObjects, RandomObject): @@ -154,13 +157,13 @@ class MultiMixEnvironment(GridObjects, RandomObject): """ - KEYS_RESET_OPTIONS = {"time serie id"} + KEYS_RESET_OPTIONS = BaseEnv.KEYS_RESET_OPTIONS def __init__( self, envs_dir, logger=None, - experimental_read_from_local_dir=False, + experimental_read_from_local_dir=None, n_busbar=DEFAULT_N_BUSBAR_PER_SUB, _add_to_name="", # internal, for test only, do not use ! _compat_glop_version=None, # internal, for test only, do not use ! @@ -174,6 +177,10 @@ def __init__( self.mix_envs = [] self._env_dir = os.path.abspath(envs_dir) self.__closed = False + self._do_not_erase_local_dir_cls = False + self._local_dir_cls = None + if not os.path.exists(envs_dir): + raise EnvError(f"There is nothing at {envs_dir}") # Special case handling for backend # TODO: with backend.copy() instead ! backendClass = None @@ -184,76 +191,170 @@ def __init__( # was introduced in grid2op 1.7.1 backend_kwargs = kwargs["backend"]._my_kwargs del kwargs["backend"] - - # Inline import to prevent cyclical import - from grid2op.MakeEnv.Make import make - + + li_mix_nms = [mix_name for mix_name in sorted(os.listdir(envs_dir)) if os.path.isdir(os.path.join(envs_dir, mix_name))] + if not li_mix_nms: + raise EnvError("We did not find any mix in this multi-mix environment.") + + # Make sure GridObject class attributes are set from first env + # Should be fine since the grid is the same for all envs + multi_env_name = (None, envs_dir, os.path.basename(os.path.abspath(envs_dir)), _add_to_name) + env_for_init = self._aux_create_a_mix(envs_dir, + li_mix_nms[0], + logger, + backendClass, + backend_kwargs, + _add_to_name, + _compat_glop_version, + n_busbar, + _test, + experimental_read_from_local_dir, + multi_env_name, + kwargs) + + cls_res_me = self._aux_add_class_file(env_for_init) + if cls_res_me is not None: + self.__class__ = cls_res_me + else: + self.__class__ = type(self).init_grid(type(env_for_init.backend), _local_dir_cls=env_for_init._local_dir_cls) + self.mix_envs.append(env_for_init) + self._local_dir_cls = env_for_init._local_dir_cls + # TODO reuse same observation_space and action_space in all the envs maybe ? + multi_env_name = (type(env_for_init)._PATH_GRID_CLASSES, *multi_env_name[1:]) try: - for env_dir in sorted(os.listdir(envs_dir)): - env_path = os.path.join(envs_dir, env_dir) - if not os.path.isdir(env_path): + for mix_name in li_mix_nms[1:]: + mix_path = os.path.join(envs_dir, mix_name) + if not os.path.isdir(mix_path): continue - this_logger = ( - logger.getChild(f"MultiMixEnvironment_{env_dir}") - if logger is not None - else None - ) - # Special case for backend - if backendClass is not None: - try: - # should pass with grid2op >= 1.7.1 - bk = backendClass(**backend_kwargs) - except TypeError as exc_: - # with grid2Op version prior to 1.7.1 - # you might have trouble with - # "TypeError: __init__() got an unexpected keyword argument 'can_be_copied'" - msg_ = ("Impossible to create a backend for each mix using the " - "backend key-word arguments. Falling back to creating " - "with no argument at all (default behaviour with grid2op <= 1.7.0).") - warnings.warn(msg_) - bk = backendClass() - env = make( - env_path, - backend=bk, - _add_to_name=_add_to_name, - _compat_glop_version=_compat_glop_version, - n_busbar=n_busbar, - test=_test, - logger=this_logger, - experimental_read_from_local_dir=experimental_read_from_local_dir, - **kwargs, - ) - else: - env = make( - env_path, - n_busbar=n_busbar, - _add_to_name=_add_to_name, - _compat_glop_version=_compat_glop_version, - test=_test, - logger=this_logger, - experimental_read_from_local_dir=experimental_read_from_local_dir, - **kwargs, - ) - self.mix_envs.append(env) + mix = self._aux_create_a_mix(envs_dir, + mix_name, + logger, + backendClass, + backend_kwargs, + _add_to_name, + _compat_glop_version, + n_busbar, + _test, + experimental_read_from_local_dir, + multi_env_name, + kwargs) + self.mix_envs.append(mix) except Exception as exc_: - err_msg = "MultiMix environment creation failed: {}".format(exc_) - raise EnvError(err_msg) + err_msg = "MultiMix environment creation failed at the creation of the first mix. Error: {}".format(exc_) + raise EnvError(err_msg) from exc_ if len(self.mix_envs) == 0: err_msg = "MultiMix envs_dir did not contain any valid env" raise EnvError(err_msg) + # tell every mix the "MultiMix" is responsible for deleting the + # folder that stores the classes definition + for el in self.mix_envs: + el._do_not_erase_local_dir_cls = True self.env_index = 0 self.current_env = self.mix_envs[self.env_index] - # Make sure GridObject class attributes are set from first env - # Should be fine since the grid is the same for all envs - multi_env_name = os.path.basename(os.path.abspath(envs_dir)) + _add_to_name - save_env_name = self.current_env.env_name - self.current_env.env_name = multi_env_name - self.__class__ = self.init_grid(self.current_env) - self.current_env.env_name = save_env_name + # legacy behaviour (using experimental_read_from_local_dir kwargs in env.make) + if self._read_from_local_dir is not None: + if os.path.split(self._read_from_local_dir)[1] == "_grid2op_classes": + self._do_not_erase_local_dir_cls = True + else: + self._do_not_erase_local_dir_cls = True + + def _aux_aux_add_class_file(self, sys_path, env_for_init): + # used for the old behaviour (setting experimental_read_from_local_dir=True in make) + bk_type = type(env_for_init.backend) + _PATH_GRID_CLASSES = bk_type._PATH_GRID_CLASSES + cls_res_me = None + try: + bk_type._PATH_GRID_CLASSES = None + my_type_tmp = MultiMixEnvironment.init_grid(gridobj=bk_type, _local_dir_cls=None) + txt_, cls_res_me = BaseEnv._aux_gen_classes(my_type_tmp, + sys_path, + _add_class_output=True) + # then add the class to the init file + with open(os.path.join(sys_path, "__init__.py"), "a", encoding="utf-8") as f: + f.write(txt_) + finally: + # make sure to put back the correct _PATH_GRID_CLASSES + bk_type._PATH_GRID_CLASSES = _PATH_GRID_CLASSES + return cls_res_me + + def _aux_add_class_file(self, env_for_init): + # used for the "new" bahviour for grid2op make (automatic read from local dir) + if env_for_init.classes_are_in_files() and env_for_init._local_dir_cls is not None: + sys_path = os.path.abspath(env_for_init._local_dir_cls.name) + self._local_dir_cls = env_for_init._local_dir_cls + env_for_init._local_dir_cls = None + # then generate the proper classes + cls_res_me = self._aux_aux_add_class_file(sys_path, env_for_init) + return cls_res_me + return None + + def _aux_create_a_mix(self, + envs_dir, + mix_name, + logger, + backendClass, + backend_kwargs, + _add_to_name, + _compat_glop_version, + n_busbar, + _test, + experimental_read_from_local_dir, + multi_env_name, + kwargs + ): + # Inline import to prevent cyclical import + from grid2op.MakeEnv.Make import make + + this_logger = ( + logger.getChild(f"MultiMixEnvironment_{mix_name}") + if logger is not None + else None + ) + mix_path = os.path.join(envs_dir, mix_name) + # Special case for backend + if backendClass is not None: + try: + # should pass with grid2op >= 1.7.1 + bk = backendClass(**backend_kwargs) + except TypeError as exc_: + # with grid2Op version prior to 1.7.1 + # you might have trouble with + # "TypeError: __init__() got an unexpected keyword argument 'can_be_copied'" + msg_ = ("Impossible to create a backend for each mix using the " + "backend key-word arguments. Falling back to creating " + "with no argument at all (default behaviour with grid2op <= 1.7.0).") + warnings.warn(msg_) + bk = backendClass() + mix = make( + mix_path, + backend=bk, + _add_to_name=_add_to_name, + _compat_glop_version=_compat_glop_version, + n_busbar=n_busbar, + test=_test, + logger=this_logger, + experimental_read_from_local_dir=experimental_read_from_local_dir, + _overload_name_multimix=multi_env_name, + **kwargs, + ) + else: + mix = make( + mix_path, + n_busbar=n_busbar, + _add_to_name=_add_to_name, + _compat_glop_version=_compat_glop_version, + test=_test, + logger=this_logger, + experimental_read_from_local_dir=experimental_read_from_local_dir, + _overload_name_multimix=multi_env_name, + **kwargs, + ) + return mix + def get_path_env(self): """ Get the path that allows to create this environment. @@ -304,11 +405,13 @@ def __next__(self): def __getattr__(self, name): # TODO what if name is an integer ? make it possible to loop with integer here + if self.__closed: + raise EnvError("This environment is closed, you cannot use it.") return getattr(self.current_env, name) def keys(self): for mix in self.mix_envs: - yield mix.name + yield mix.multimix_mix_name def values(self): for mix in self.mix_envs: @@ -316,7 +419,7 @@ def values(self): def items(self): for mix in self.mix_envs: - yield mix.name, mix + yield mix.multimix_mix_name, mix def copy(self): if self.__closed: @@ -326,6 +429,11 @@ def copy(self): current_env = self.current_env self.current_env = None + # do not copy these attributes + _local_dir_cls = self._local_dir_cls + self._local_dir_cls = None + + # create the new object and copy the normal attribute cls = self.__class__ res = cls.__new__(cls) for k in self.__dict__: @@ -333,11 +441,17 @@ def copy(self): # this is handled elsewhere continue setattr(res, k, copy.deepcopy(getattr(self, k))) + # now deal with the mixes res.mix_envs = [mix.copy() for mix in mix_envs] res.current_env = res.mix_envs[res.env_index] - + # finally deal with the ownership of the class folder + res._local_dir_cls = _local_dir_cls + res._do_not_erase_local_dir_cls = True + + # put back attributes of `self` that have been put aside self.mix_envs = mix_envs self.current_env = current_env + self._local_dir_cls = _local_dir_cls return res def __getitem__(self, key): @@ -360,7 +474,7 @@ def __getitem__(self, key): raise EnvError("This environment is closed, you cannot use it.") # Search for key for mix in self.mix_envs: - if mix.name == key: + if mix.multimix_mix_name == key: return mix # Not found by name @@ -370,7 +484,7 @@ def reset(self, *, seed: Union[int, None] = None, random=False, - options: Union[Dict[Union[str, Literal["time serie id"]], Union[int, str]], None] = None) -> BaseObservation: + options: RESET_OPTIONS_TYPING = None) -> BaseObservation: if self.__closed: raise EnvError("This environment is closed, you cannot use it.") @@ -389,13 +503,7 @@ def reset(self, self.env_index = (self.env_index + 1) % len(self.mix_envs) self.current_env = self.mix_envs[self.env_index] - - if options is not None and "time serie id" in options: - self.set_id(options["time serie id"]) - - if seed is not None: - self.seed(seed) - return self.current_env.reset() + return self.current_env.reset(seed=seed, options=options) def seed(self, seed=None): """ @@ -490,7 +598,17 @@ def close(self): for mix in self.mix_envs: mix.close() + self.__closed = True + + # free the resources (temporary directory) + if self._do_not_erase_local_dir_cls: + # The resources are not held by this env, so + # I do not remove them + # (case for ObsEnv or ForecastedEnv) + return + BaseEnv._aux_close_local_dir_cls(self) + def attach_layout(self, grid_layout): if self.__closed: @@ -504,7 +622,12 @@ def __del__(self): self.close() def generate_classes(self): - # TODO this is not really a good idea, as the multi-mix itself is not read from the - # files ! - for mix in self.mix_envs: - mix.generate_classes() + mix_for_classes = self.mix_envs[0] + path_cls = os.path.join(mix_for_classes.get_path_env(), "_grid2op_classes") + if not os.path.exists(path_cls): + try: + os.mkdir(path_cls) + except FileExistsError: + pass + mix_for_classes.generate_classes() + self._aux_aux_add_class_file(path_cls, mix_for_classes) diff --git a/grid2op/Environment/timedOutEnv.py b/grid2op/Environment/timedOutEnv.py index 2b7c16d85..a1952f99a 100644 --- a/grid2op/Environment/timedOutEnv.py +++ b/grid2op/Environment/timedOutEnv.py @@ -9,12 +9,14 @@ import time from math import floor from typing import Any, Dict, Tuple, Union, List, Literal - +import os + from grid2op.Environment.environment import Environment from grid2op.Action import BaseAction from grid2op.Observation import BaseObservation from grid2op.Exceptions import EnvError from grid2op.Space import DEFAULT_N_BUSBAR_PER_SUB +from grid2op.MakeEnv.PathUtils import USE_CLASS_IN_FILE class TimedOutEnvironment(Environment): # TODO heritage ou alors on met un truc de base @@ -71,7 +73,19 @@ def __init__(self, self._nb_dn_last = 0 self._is_init_dn = False if isinstance(grid2op_env, Environment): - super().__init__(**grid2op_env.get_kwargs()) + kwargs = grid2op_env.get_kwargs() + if grid2op_env.classes_are_in_files(): + # I need to build the classes + + # first take the "ownership" of the tmp directory + kwargs["_local_dir_cls"] = grid2op_env._local_dir_cls + grid2op_env._local_dir_cls = None + + # then generate the proper classes + sys_path = os.path.abspath(kwargs["_local_dir_cls"].name) + bk_type = type(grid2op_env.backend) + self._add_classes_in_files(sys_path, bk_type, grid2op_env.classes_are_in_files()) + super().__init__(**kwargs) elif isinstance(grid2op_env, dict): super().__init__(**grid2op_env) else: @@ -182,6 +196,7 @@ def get_params_for_runner(self): @classmethod def init_obj_from_kwargs(cls, + *, other_env_kwargs, init_env_path, init_grid_path, @@ -215,41 +230,51 @@ def init_obj_from_kwargs(cls, observation_bk_kwargs, _raw_backend_class, _read_from_local_dir, + _local_dir_cls, + _overload_name_multimix, n_busbar=DEFAULT_N_BUSBAR_PER_SUB): - res = TimedOutEnvironment(grid2op_env={"init_env_path": init_env_path, - "init_grid_path": init_grid_path, - "chronics_handler": chronics_handler, - "backend": backend, - "parameters": parameters, - "name": name, - "names_chronics_to_backend": names_chronics_to_backend, - "actionClass": actionClass, - "observationClass": observationClass, - "rewardClass": rewardClass, - "legalActClass": legalActClass, - "voltagecontrolerClass": voltagecontrolerClass, - "other_rewards": other_rewards, - "opponent_space_type": opponent_space_type, - "opponent_action_class": opponent_action_class, - "opponent_class": opponent_class, - "opponent_init_budget": opponent_init_budget, - "opponent_budget_per_ts": opponent_budget_per_ts, - "opponent_budget_class": opponent_budget_class, - "opponent_attack_duration": opponent_attack_duration, - "opponent_attack_cooldown": opponent_attack_cooldown, - "kwargs_opponent": kwargs_opponent, - "with_forecast": with_forecast, - "attention_budget_cls": attention_budget_cls, - "kwargs_attention_budget": kwargs_attention_budget, - "has_attention_budget": has_attention_budget, - "logger": logger, - "kwargs_observation": kwargs_observation, - "observation_bk_class": observation_bk_class, - "observation_bk_kwargs": observation_bk_kwargs, - "_raw_backend_class": _raw_backend_class, - "_read_from_local_dir": _read_from_local_dir, - "n_busbar": int(n_busbar)}, - **other_env_kwargs) + grid2op_env={"init_env_path": init_env_path, + "init_grid_path": init_grid_path, + "chronics_handler": chronics_handler, + "backend": backend, + "parameters": parameters, + "name": name, + "names_chronics_to_backend": names_chronics_to_backend, + "actionClass": actionClass, + "observationClass": observationClass, + "rewardClass": rewardClass, + "legalActClass": legalActClass, + "voltagecontrolerClass": voltagecontrolerClass, + "other_rewards": other_rewards, + "opponent_space_type": opponent_space_type, + "opponent_action_class": opponent_action_class, + "opponent_class": opponent_class, + "opponent_init_budget": opponent_init_budget, + "opponent_budget_per_ts": opponent_budget_per_ts, + "opponent_budget_class": opponent_budget_class, + "opponent_attack_duration": opponent_attack_duration, + "opponent_attack_cooldown": opponent_attack_cooldown, + "kwargs_opponent": kwargs_opponent, + "with_forecast": with_forecast, + "attention_budget_cls": attention_budget_cls, + "kwargs_attention_budget": kwargs_attention_budget, + "has_attention_budget": has_attention_budget, + "logger": logger, + "kwargs_observation": kwargs_observation, + "observation_bk_class": observation_bk_class, + "observation_bk_kwargs": observation_bk_kwargs, + "_raw_backend_class": _raw_backend_class, + "_read_from_local_dir": _read_from_local_dir, + "n_busbar": int(n_busbar), + "_local_dir_cls": _local_dir_cls, + "_overload_name_multimix": _overload_name_multimix} + if not "time_out_ms" in other_env_kwargs: + raise EnvError("You cannot make a MaskedEnvironment without providing the list of lines of interest") + for el in other_env_kwargs: + if el == "time_out_ms": + continue + warnings.warn(f"kwargs {el} provided to make the environment will be ignored") + res = TimedOutEnvironment(grid2op_env, time_out_ms=other_env_kwargs["time_out_ms"]) return res diff --git a/grid2op/Episode/CompactEpisodeData.py b/grid2op/Episode/CompactEpisodeData.py index 30a138311..e5cdabf9d 100644 --- a/grid2op/Episode/CompactEpisodeData.py +++ b/grid2op/Episode/CompactEpisodeData.py @@ -300,3 +300,17 @@ def list_episode(path): def __len__(self): return self.game_over_timestep + + def make_serializable(self): + """ + INTERNAL + + .. warning:: /!\\\\ Internal, do not use unless you know what you are doing /!\\\\ + Used by he runner to serialize properly an episode + + Called in the _aux_run_one_episode (one of the Runner auxilliary function) to make + sure the EpisodeData can be sent back to the main process withtout issue (otherwise + there is a complain about the _ObsEnv) + """ + from grid2op.Episode.EpisodeData import EpisodeData + EpisodeData._aux_make_obs_space_serializable(self) diff --git a/grid2op/Episode/EpisodeData.py b/grid2op/Episode/EpisodeData.py index e06ac7325..1925fd7ba 100644 --- a/grid2op/Episode/EpisodeData.py +++ b/grid2op/Episode/EpisodeData.py @@ -800,6 +800,48 @@ def to_disk(self): dict_ = {"version": f"{grid2op.__version__}"} json.dump(obj=dict_, fp=f, indent=4, sort_keys=True) + def _aux_make_obs_space_serializable(self): + """I put it here because it's also used by CompactEpisodeData. + + The only requirement is that `self` has an attribute `observation_space` which is a + valid grid2op ObservationSpace""" + if self.observation_space is None: + return + from grid2op.Environment._obsEnv import _ObsEnv + # remove the observation_env of the observation_space + self.observation_space = self.observation_space.copy(copy_backend=True) + self.observation_space._backend_obs.close() + self.observation_space._backend_obs = None + self.observation_space.obs_env.close() + self.observation_space.obs_env = None + self.observation_space._ObsEnv_class = _ObsEnv + self.observation_space._real_env_kwargs = None + self.observation_space._template_obj._obs_env = None + self.observation_space._template_obj._ptr_kwargs_env = None + self.observation_space._empty_obs._obs_env = None + self.observation_space._empty_obs._ptr_kwargs_env = None + self.observation_space._deactivate_simulate(None) + + def make_serializable(self): + """ + INTERNAL + + .. warning:: /!\\\\ Internal, do not use unless you know what you are doing /!\\\\ + Used by he runner to serialize properly an episode + + Called in the _aux_run_one_episode (one of the Runner auxilliary function) to make + sure the EpisodeData can be sent back to the main process withtout issue (otherwise + there is a complain about the _ObsEnv) + """ + self._aux_make_obs_space_serializable() + # remove the observation_env of the observation + for el in self.observations.objects: + if el is not None: + el._obs_env = None + el._ptr_kwargs_env = None + + self.observations.helper = self.observation_space + @staticmethod def get_grid2op_version(path_episode): """ diff --git a/grid2op/MakeEnv/Make.py b/grid2op/MakeEnv/Make.py index 4692c6743..15bd5b6c3 100644 --- a/grid2op/MakeEnv/Make.py +++ b/grid2op/MakeEnv/Make.py @@ -250,12 +250,14 @@ def _aux_make_multimix( n_busbar=2, _add_to_name="", _compat_glop_version=None, + _overload_name_multimix=None, logger=None, **kwargs ) -> Environment: # Local import to prevent imports loop from grid2op.Environment import MultiMixEnvironment - + if _overload_name_multimix is not None: + raise RuntimeError("You should not create a MultiMix with `_overload_name_multimix`.") return MultiMixEnvironment( dataset_path, experimental_read_from_local_dir=experimental_read_from_local_dir, @@ -268,6 +270,15 @@ def _aux_make_multimix( ) +def _get_path_multimix(_overload_name_multimix) -> str: + baseenv_path, multi_mix_name, add_to_name = _overload_name_multimix + if os.path.exists(baseenv_path): + return baseenv_path + if multi_mix_name in TEST_DEV_ENVS: + return TEST_DEV_ENVS[multi_mix_name] + raise Grid2OpException(f"Unknown multimix environment with name {multi_mix_name} that should be located at {baseenv_path}.") + + def make( dataset : Union[str, os.PathLike], *, @@ -277,6 +288,7 @@ def make( n_busbar=2, _add_to_name : str="", _compat_glop_version : Optional[str]=None, + _overload_name_multimix : Optional[str]=None, # do not use ! **kwargs ) -> Environment: """ @@ -327,6 +339,9 @@ def make( _compat_glop_version: Internal, do not use (and can only be used when setting "test=True") + + _overload_name_multimix: + Internal, do not use ! Returns ------- @@ -419,6 +434,7 @@ def make_from_path_fn_(*args, **kwargs): dataset_path=dataset, _add_to_name=_add_to_name_tmp, _compat_glop_version=_compat_glop_version_tmp, + _overload_name_multimix=_overload_name_multimix, n_busbar=n_busbar, **kwargs ) @@ -430,7 +446,7 @@ def make_from_path_fn_(*args, **kwargs): ) # Unknown dev env - if test and dataset_name not in TEST_DEV_ENVS: + if _overload_name_multimix is None and test and dataset_name not in TEST_DEV_ENVS: raise Grid2OpException(_MAKE_UNKNOWN_ENV.format(dataset)) # Known test env and test flag enabled @@ -443,7 +459,13 @@ def make_from_path_fn_(*args, **kwargs): or dataset_name.startswith("educ") ): warnings.warn(_MAKE_DEV_ENV_DEPRECATED_WARN.format(dataset_name)) - ds_path = TEST_DEV_ENVS[dataset_name] + if _overload_name_multimix: + # make is invoked from a Multimix + path_multimix = _get_path_multimix(_overload_name_multimix) + ds_path = os.path.join(path_multimix, dataset_name) + else: + # normal behaviour + ds_path = TEST_DEV_ENVS[dataset_name] # Check if multimix from path if _aux_is_multimix(ds_path): @@ -463,6 +485,7 @@ def make_from_path_fn_(*args, **kwargs): _add_to_name=_add_to_name, _compat_glop_version=_compat_glop_version, experimental_read_from_local_dir=experimental_read_from_local_dir, + _overload_name_multimix=_overload_name_multimix, **kwargs ) @@ -475,6 +498,7 @@ def make_from_path_fn_(*args, **kwargs): logger=logger, n_busbar=n_busbar, experimental_read_from_local_dir=experimental_read_from_local_dir, + _overload_name_multimix=_overload_name_multimix, **kwargs ) @@ -494,5 +518,6 @@ def make_from_path_fn_(*args, **kwargs): logger=logger, n_busbar=n_busbar, experimental_read_from_local_dir=experimental_read_from_local_dir, + _overload_name_multimix=_overload_name_multimix, **kwargs ) diff --git a/grid2op/MakeEnv/MakeFromPath.py b/grid2op/MakeEnv/MakeFromPath.py index c550261be..ff85d56f7 100644 --- a/grid2op/MakeEnv/MakeFromPath.py +++ b/grid2op/MakeEnv/MakeFromPath.py @@ -8,6 +8,7 @@ import os import time +import copy import importlib.util import numpy as np import json @@ -34,6 +35,8 @@ from grid2op.operator_attention import LinearAttentionBudget from grid2op.MakeEnv.get_default_aux import _get_default_aux +from grid2op.MakeEnv.PathUtils import _aux_fix_backend_internal_classes + DIFFICULTY_NAME = "difficulty" CHALLENGE_NAME = "competition" @@ -93,7 +96,9 @@ "obs.simulate and obs.get_forecasted_env). If provided, this should " "be a type / class and not an instance of this class. (by default it's None)"), "observation_backend_kwargs": ("key-word arguments to build the observation backend (used for Simulator, " - " obs.simulate and obs.get_forecasted_env). This should be a dictionnary. (by default it's None)") + " obs.simulate and obs.get_forecasted_env). This should be a dictionnary. (by default it's None)"), + "class_in_file": ("experimental: tell grid2op to store the classes generated in the hard drive " + "which can solve lots of pickle / multi processing related issue"), } NAME_CHRONICS_FOLDER = "chronics" @@ -124,6 +129,7 @@ def make_from_dataset_path( n_busbar=2, _add_to_name="", _compat_glop_version=None, + _overload_name_multimix=None, **kwargs, ) -> Environment: """ @@ -873,7 +879,15 @@ def make_from_dataset_path( # new in 1.10.2 : allow_loaded_backend = False classes_path = None - if USE_CLASS_IN_FILE: + init_env = None + this_local_dir = None + use_class_in_files = USE_CLASS_IN_FILE + if "class_in_file" in kwargs: + classes_in_file_kwargs = bool(kwargs["class_in_file"]) + use_class_in_files = classes_in_file_kwargs + + if use_class_in_files: + # new behaviour sys_path = os.path.join(os.path.split(grid_path_abs)[0], "_grid2op_classes") if not os.path.exists(sys_path): try: @@ -881,39 +895,38 @@ def make_from_dataset_path( except FileExistsError: # if another process created it, no problem pass + init_nm = os.path.join(sys_path, "__init__.py") + if not os.path.exists(init_nm): + try: + with open(init_nm, "w", encoding="utf-8") as f: + f.write("This file has been created by grid2op in a `env.make(...)` call. Do not modify it or remove it") + except FileExistsError: + pass - # TODO: automatic delete the directory if needed - - # TODO: check the "new" path works - - # TODO: in the BaseEnv.generate_classes make sure the classes are added to the "__init__" if the file is created - # TODO: make that only if backend can be copied ! + import tempfile + this_local_dir = tempfile.TemporaryDirectory(dir=sys_path) + if experimental_read_from_local_dir: + warnings.warn("With the automatic class generation, we removed the possibility to " + "set `experimental_read_from_local_dir` to True.") + experimental_read_from_local_dir = False # TODO: check the hash thingy is working in baseEnv._aux_gen_classes (currently a pdb) - # TODO: check that previous behaviour is working correctly - - # TODO: create again the environment with the proper "read from local_dir" - # TODO check that it works if the backend changes, if shunt / no_shunt if name of env changes etc. # TODO: what if it cannot write on disk => fallback to previous behaviour + data_feeding_fake = copy.deepcopy(data_feeding) + data_feeding_fake.cleanup_action_space() - # TODO: allow for a way to disable that (with env variable or config in grid2op) - # TODO: keep only one environment that will delete the files (with a flag in its constructor) - - # TODO: explain in doc new behaviour with regards to "class in file" - - # TODO: basic CI for this "new" mode - - # TODO: use the tempfile.TemporaryDirectory() to hold the classes, and in the (real) env copy, runner , env.get_kwargs() - # or whatever - # reference this "tempfile.TemporaryDirectory()" which will be deleted automatically - # when every "pointer" to it are deleted, this sounds more reasonable - if not experimental_read_from_local_dir: - init_env = Environment(init_env_path=os.path.abspath(dataset_path), + # Set graph layout if not None and not an empty dict + if graph_layout is not None and graph_layout: + type(backend).attach_layout(graph_layout) + + if not os.path.exists(this_local_dir.name): + raise EnvError(f"Path {this_local_dir.name} has not been created by the tempfile package") + init_env = Environment(init_env_path=os.path.abspath(dataset_path), init_grid_path=grid_path_abs, - chronics_handler=data_feeding, + chronics_handler=data_feeding_fake, backend=backend, parameters=param, name=name_env + _add_to_name, @@ -940,25 +953,36 @@ def make_from_dataset_path( n_busbar=n_busbar, # TODO n_busbar_per_sub different num per substations: read from a config file maybe (if not provided by the user) _compat_glop_version=_compat_glop_version, _read_from_local_dir=None, # first environment to generate the classes and save them + _local_dir_cls=None, + _overload_name_multimix=_overload_name_multimix, kwargs_observation=kwargs_observation, observation_bk_class=observation_backend_class, - observation_bk_kwargs=observation_backend_kwargs, - ) - this_local_dir = f"{time.time()}_{os.getpid()}" - init_env.generate_classes(local_dir_id=this_local_dir) - init_env.backend = None # to avoid to close the backend when init_env is deleted - classes_path = os.path.join(sys_path, this_local_dir) - # to force the reading back of the classes from the hard drive - init_env._forget_classes() # TODO not implemented - init_env.close() - else: - classes_path = sys_path + observation_bk_kwargs=observation_backend_kwargs + ) + if not os.path.exists(this_local_dir.name): + raise EnvError(f"Path {this_local_dir.name} has not been created by the tempfile package") + init_env.generate_classes(local_dir_id=this_local_dir.name) + # fix `my_bk_act_class` and `_complete_action_class` + _aux_fix_backend_internal_classes(type(backend), this_local_dir) + init_env.backend = None # to avoid to close the backend when init_env is deleted + init_env._local_dir_cls = None + classes_path = this_local_dir.name allow_loaded_backend = True else: # legacy behaviour (<= 1.10.1 behaviour) classes_path = None if not experimental_read_from_local_dir else experimental_read_from_local_dir if experimental_read_from_local_dir: - sys_path = os.path.join(os.path.split(grid_path_abs)[0], "_grid2op_classes") + if _overload_name_multimix is not None: + # I am in a multimix + if _overload_name_multimix[0] is None: + # first mix: path is correct + sys_path = os.path.join(os.path.split(grid_path_abs)[0], "_grid2op_classes") + else: + # other mixes I need to retrieve the properties of the first mix + sys_path = _overload_name_multimix[0] + else: + # I am not in a multimix + sys_path = os.path.join(os.path.split(grid_path_abs)[0], "_grid2op_classes") if not os.path.exists(sys_path): raise RuntimeError( "Attempting to load the grid classes from the env path. Yet the directory " @@ -974,9 +998,11 @@ def make_from_dataset_path( f'Please remove "{sys_path}" and call `env.generate_classes()` where env is an ' f"environment created with `experimental_read_from_local_dir=False` (default)" ) - + import sys + sys.path.append(os.path.split(os.path.abspath(sys_path))[0]) + classes_path = sys_path # Finally instantiate env from config & overrides - # including (if activated the new grid2op behaviour) + # including (if activated the new grid2op behaviour) env = Environment( init_env_path=os.path.abspath(dataset_path), init_grid_path=grid_path_abs, @@ -1008,15 +1034,16 @@ def make_from_dataset_path( _compat_glop_version=_compat_glop_version, _read_from_local_dir=classes_path, _allow_loaded_backend=allow_loaded_backend, + _local_dir_cls=this_local_dir, + _overload_name_multimix=_overload_name_multimix, kwargs_observation=kwargs_observation, observation_bk_class=observation_backend_class, - observation_bk_kwargs=observation_backend_kwargs, - ) - + observation_bk_kwargs=observation_backend_kwargs + ) # Update the thermal limit if any if thermal_limits is not None: env.set_thermal_limit(thermal_limits) - + # Set graph layout if not None and not an empty dict if graph_layout is not None and graph_layout: env.attach_layout(graph_layout) diff --git a/grid2op/MakeEnv/PathUtils.py b/grid2op/MakeEnv/PathUtils.py index 33611eefa..ece6a551f 100644 --- a/grid2op/MakeEnv/PathUtils.py +++ b/grid2op/MakeEnv/PathUtils.py @@ -18,7 +18,7 @@ KEY_DATA_PATH = "data_path" KEY_CLASS_IN_FILE = "class_in_file" - +KEY_CLASS_IN_FILE_ENV_VAR = f"grid2op_{KEY_CLASS_IN_FILE}" def str_to_bool(string: str) -> bool: """convert a "string" to a boolean, with the convention: @@ -46,13 +46,12 @@ def str_to_bool(string: str) -> bool: if KEY_CLASS_IN_FILE in dict_: USE_CLASS_IN_FILE = bool(dict_[KEY_CLASS_IN_FILE]) - if KEY_CLASS_IN_FILE in os.environ: + if KEY_CLASS_IN_FILE_ENV_VAR in os.environ: try: - USE_CLASS_IN_FILE = str_to_bool(os.environ[KEY_CLASS_IN_FILE]) + USE_CLASS_IN_FILE = str_to_bool(os.environ[KEY_CLASS_IN_FILE_ENV_VAR]) except ValueError as exc: - raise RuntimeError(f"Impossible to read the behaviour from `{KEY_CLASS_IN_FILE}` environment variable") from exc - - USE_CLASS_IN_FILE = False # deactivated until further notice + raise RuntimeError(f"Impossible to read the behaviour from `{KEY_CLASS_IN_FILE_ENV_VAR}` environment variable") from exc + def _create_path_folder(data_path): if not os.path.exists(data_path): @@ -65,3 +64,10 @@ def _create_path_folder(data_path): 'and set the "data_path" to point to a path where you can store data.' "".format(data_path, DEFAULT_PATH_CONFIG) ) + + +def _aux_fix_backend_internal_classes(backend_cls, this_local_dir): + # fix `my_bk_act_class` and `_complete_action_class` + backend_cls._add_internal_classes(this_local_dir) + tmp = {} + backend_cls._make_cls_dict_extended(backend_cls, tmp, as_list=False) diff --git a/grid2op/Observation/baseObservation.py b/grid2op/Observation/baseObservation.py index 513b0ccfa..e1c1016ca 100644 --- a/grid2op/Observation/baseObservation.py +++ b/grid2op/Observation/baseObservation.py @@ -3389,7 +3389,7 @@ def simulate(self, action : "grid2op.Action.BaseAction", time_step:int=1) -> Tup sim_obs._update_internal_env_params(self._obs_env) return (sim_obs, *rest) # parentheses are needed for python 3.6 at least. - def copy(self) -> Self: + def copy(self, env=None) -> Self: """ INTERNAL @@ -3418,14 +3418,19 @@ def copy(self) -> Self: res = copy.deepcopy(self) self._obs_env = obs_env - res._obs_env = obs_env - self.action_helper = action_helper - res.action_helper = action_helper - self._ptr_kwargs_env = _ptr_kwargs_env - res._ptr_kwargs_env = _ptr_kwargs_env - + if env is None: + # this will make a copy but the observation will still + # be "bound" to the original env + res._obs_env = obs_env + res.action_helper = action_helper + res._ptr_kwargs_env = _ptr_kwargs_env + else: + # the action will be "bound" to the new environment + res._obs_env = env._observation_space.obs_env + res.action_helper = env._observation_space.action_helper_env + res._ptr_kwargs_env = env._observation_space._real_env_kwargs return res @property @@ -4704,6 +4709,7 @@ def _make_env_from_arays(self, prod_p=prod_p, prod_v=prod_v, maintenance=maintenance) + ch.max_iter = ch.real_data.max_iter backend = self._obs_env.backend.copy() backend._is_loaded = True diff --git a/grid2op/Observation/observationSpace.py b/grid2op/Observation/observationSpace.py index 8eeebd89a..5b4a00d95 100644 --- a/grid2op/Observation/observationSpace.py +++ b/grid2op/Observation/observationSpace.py @@ -72,6 +72,7 @@ def __init__( observation_bk_kwargs=None, logger=None, _with_obs_env=True, # pass + _local_dir_cls=None, ): """ INTERNAL @@ -80,22 +81,14 @@ def __init__( Env: requires :attr:`grid2op.Environment.BaseEnv.parameters` and :attr:`grid2op.Environment.BaseEnv.backend` to be valid """ - - # lazy import to prevent circular references (Env -> Observation -> Obs Space -> _ObsEnv -> Env) - from grid2op.Environment._obsEnv import _ObsEnv - if actionClass is None: from grid2op.Action import CompleteAction actionClass = CompleteAction - if logger is None: - self.logger = logging.getLogger(__name__) - self.logger.disabled = True - else: - self.logger: logging.Logger = logger.getChild("grid2op_ObsSpace") self._init_observationClass = observationClass SerializableObservationSpace.__init__( - self, gridobj, observationClass=observationClass + self, gridobj, observationClass=observationClass, _local_dir_cls=_local_dir_cls, + logger=logger, ) self.with_forecast = with_forecast self._simulate_parameters = copy.deepcopy(env.parameters) @@ -112,14 +105,9 @@ def __init__( self.reward_helper = RewardHelper(reward_func=self._reward_func, logger=self.logger) self.__can_never_use_simulate = False - # TODO here: have another backend class maybe - _with_obs_env = _with_obs_env and self._create_backend_obs(env, observation_bk_class, observation_bk_kwargs) - - self._ObsEnv_class = _ObsEnv.init_grid( - type(env.backend), force_module=_ObsEnv.__module__ - ) - self._ObsEnv_class._INIT_GRID_CLS = _ObsEnv # otherwise it's lost - setattr(sys.modules[_ObsEnv.__module__], self._ObsEnv_class.__name__, self._ObsEnv_class) + _with_obs_env = _with_obs_env and self._create_backend_obs(env, observation_bk_class, observation_bk_kwargs, _local_dir_cls) + + self._ObsEnv_class = None if _with_obs_env: self._create_obs_env(env, observationClass) self.reward_helper.initialize(self.obs_env) @@ -175,6 +163,18 @@ def set_real_env_kwargs(self, env): del self._real_env_kwargs["observation_bk_kwargs"] def _create_obs_env(self, env, observationClass): + if self._ObsEnv_class is None: + # lazy import to prevent circular references (Env -> Observation -> Obs Space -> _ObsEnv -> Env) + from grid2op.Environment._obsEnv import _ObsEnv + + # self._ObsEnv_class = _ObsEnv.init_grid( + # type(env.backend), force_module=_ObsEnv.__module__, force=_local_dir_cls is not None + # ) + # self._ObsEnv_class._INIT_GRID_CLS = _ObsEnv # otherwise it's lost + self._ObsEnv_class = _ObsEnv.init_grid( + type(env.backend), _local_dir_cls=env._local_dir_cls + ) + self._ObsEnv_class._INIT_GRID_CLS = _ObsEnv # otherwise it's lost other_rewards = {k: v.rewardClass for k, v in env.other_rewards.items()} self.obs_env = self._ObsEnv_class( init_env_path=None, # don't leak the path of the real grid to the observation space @@ -200,14 +200,16 @@ def _create_obs_env(self, env, observationClass): highres_sim_counter=env.highres_sim_counter, _complete_action_cls=env._complete_action_cls, _ptr_orig_obs_space=self, + _local_dir_cls=env._local_dir_cls, + _read_from_local_dir=env._read_from_local_dir, ) for k, v in self.obs_env.other_rewards.items(): v.initialize(self.obs_env) - def _aux_create_backend(self, env, observation_bk_class, observation_bk_kwargs, path_grid_for): + def _aux_create_backend(self, env, observation_bk_class, observation_bk_kwargs, path_grid_for, _local_dir_cls): if observation_bk_kwargs is None: observation_bk_kwargs = env.backend._my_kwargs - observation_bk_class_used = observation_bk_class.init_grid(type(env.backend)) + observation_bk_class_used = observation_bk_class.init_grid(type(env.backend), _local_dir_cls=_local_dir_cls) self._backend_obs = observation_bk_class_used(**observation_bk_kwargs) self._backend_obs.set_env_name(env.name) self._backend_obs.load_grid(path_grid_for) @@ -216,7 +218,7 @@ def _aux_create_backend(self, env, observation_bk_class, observation_bk_kwargs, self._backend_obs.assert_grid_correct_after_powerflow() self._backend_obs.set_thermal_limit(env.get_thermal_limit()) - def _create_backend_obs(self, env, observation_bk_class, observation_bk_kwargs): + def _create_backend_obs(self, env, observation_bk_class, observation_bk_kwargs, _local_dir_cls): _with_obs_env = True path_sim_bk = os.path.join(env.get_path_env(), "grid_forecast.json") if observation_bk_class is not None or observation_bk_kwargs is not None: @@ -232,12 +234,12 @@ def _create_backend_obs(self, env, observation_bk_class, observation_bk_kwargs): path_grid_for = path_sim_bk else: path_grid_for = os.path.join(env.get_path_env(), "grid.json") - self._aux_create_backend(env, observation_bk_class, observation_bk_kwargs, path_grid_for) + self._aux_create_backend(env, observation_bk_class, observation_bk_kwargs, path_grid_for, _local_dir_cls) elif os.path.exists(path_sim_bk) and os.path.isfile(path_sim_bk): # backend used for simulate will use the same class with same args as the env # backend, but with a different grid observation_bk_class = env._raw_backend_class - self._aux_create_backend(env, observation_bk_class, observation_bk_kwargs, path_sim_bk) + self._aux_create_backend(env, observation_bk_class, observation_bk_kwargs, path_sim_bk, _local_dir_cls) elif env.backend._can_be_copied: # case where I can copy the backend for the 'simulate' and I don't need to build # it (uses same class and same grid) @@ -263,10 +265,11 @@ def _deactivate_simulate(self, env): self._backend_obs.close() self._backend_obs = None self.with_forecast = False - env.deactivate_forecast() - env.backend._can_be_copied = False - self.logger.warn("Forecasts have been deactivated because " - "the backend cannot be copied.") + if env is not None: + env.deactivate_forecast() + env.backend._can_be_copied = False + self.logger.warning("Forecasts have been deactivated because " + "the backend cannot be copied.") def reactivate_forecast(self, env): if self.__can_never_use_simulate: @@ -279,8 +282,8 @@ def reactivate_forecast(self, env): if self._backend_obs is not None: self._backend_obs.close() self._backend_obs = None - self._create_backend_obs(env, self._observation_bk_class, self._observation_bk_kwargs) - if self.obs_env is not None : + self._create_backend_obs(env, self._observation_bk_class, self._observation_bk_kwargs, env._local_dir_cls) + if self.obs_env is not None: self.obs_env.close() self.obs_env = None self._create_obs_env(env, self._init_observationClass) @@ -329,7 +332,8 @@ def _change_parameters(self, new_param): change the parameter of the "simulate" environment """ - self.obs_env.change_parameters(new_param) + if self.obs_env is not None: + self.obs_env.change_parameters(new_param) self._simulate_parameters = new_param def change_other_rewards(self, dict_reward): @@ -453,7 +457,7 @@ def reset(self, real_env): self.obs_env.reset() self._env_param = copy.deepcopy(real_env.parameters) - def _custom_deepcopy_for_copy(self, new_obj): + def _custom_deepcopy_for_copy(self, new_obj, env=None): """implements a faster "res = copy.deepcopy(self)" to use in "self.copy" Do not use it anywhere else... @@ -489,13 +493,17 @@ def _custom_deepcopy_for_copy(self, new_obj): new_obj._ptr_kwargs_observation = self._ptr_kwargs_observation # real env kwargs, these is a "pointer" anyway - new_obj._real_env_kwargs = self._real_env_kwargs + if env is not None: + from grid2op.Environment import Environment + new_obj._real_env_kwargs = Environment.get_kwargs(env, False, False) + else: + new_obj._real_env_kwargs = self._real_env_kwargs new_obj._observation_bk_class = self._observation_bk_class new_obj._observation_bk_kwargs = self._observation_bk_kwargs new_obj._ObsEnv_class = self._ObsEnv_class - def copy(self, copy_backend=False): + def copy(self, copy_backend=False, env=None): """ INTERNAL @@ -516,18 +524,23 @@ def copy(self, copy_backend=False): # create an empty "me" my_cls = type(self) res = my_cls.__new__(my_cls) - self._custom_deepcopy_for_copy(res) + self._custom_deepcopy_for_copy(res, env) if not copy_backend: res._backend_obs = backend res._empty_obs = obs_.copy() res.obs_env = obs_env else: - res.obs_env = obs_env.copy() - res.obs_env._ptr_orig_obs_space = res - res._backend_obs = res.obs_env.backend - res._empty_obs = obs_.copy() - res._empty_obs._obs_env = res.obs_env + # backend needs to be copied + if obs_env is not None: + # I also need to copy the obs env + res.obs_env = obs_env.copy(env=env, new_obs_space=res) + res._backend_obs = res.obs_env.backend + res._empty_obs = obs_.copy() + res._empty_obs._obs_env = res.obs_env + else: + # no obs env: I do nothing + res.obs_env = None # assign back the results self._backend_obs = backend diff --git a/grid2op/Observation/serializableObservationSpace.py b/grid2op/Observation/serializableObservationSpace.py index 1471a51ef..7796eb74c 100644 --- a/grid2op/Observation/serializableObservationSpace.py +++ b/grid2op/Observation/serializableObservationSpace.py @@ -6,6 +6,9 @@ # SPDX-License-Identifier: MPL-2.0 # This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. +import logging +import copy + from grid2op.Space import SerializableSpace from grid2op.Observation.completeObservation import CompleteObservation @@ -27,7 +30,7 @@ class SerializableObservationSpace(SerializableSpace): """ - def __init__(self, gridobj, observationClass=CompleteObservation, _init_grid=True): + def __init__(self, gridobj, observationClass=CompleteObservation, logger=None, _init_grid=True, _local_dir_cls=None): """ Parameters @@ -40,16 +43,26 @@ def __init__(self, gridobj, observationClass=CompleteObservation, _init_grid=Tru """ SerializableSpace.__init__( - self, gridobj=gridobj, subtype=observationClass, _init_grid=_init_grid + self, gridobj=gridobj, + subtype=observationClass, + _init_grid=_init_grid, + _local_dir_cls=_local_dir_cls ) self.observationClass = self.subtype self._empty_obs = self._template_obj + + if logger is None: + self.logger = logging.getLogger(__name__) + self.logger.disabled = True + else: + self.logger: logging.Logger = logger.getChild("grid2op_ObsSpace") def _custom_deepcopy_for_copy(self, new_obj): super()._custom_deepcopy_for_copy(new_obj) # SerializableObservationSpace new_obj.observationClass = self.observationClass # const new_obj._empty_obs = self._template_obj # const + new_obj.logger = copy.deepcopy(self.logger) @staticmethod def from_dict(dict_): diff --git a/grid2op/Opponent/opponentSpace.py b/grid2op/Opponent/opponentSpace.py index 60d3a9927..bca588d46 100644 --- a/grid2op/Opponent/opponentSpace.py +++ b/grid2op/Opponent/opponentSpace.py @@ -49,6 +49,7 @@ def __init__( attack_cooldown, # minimum duration between two consecutive attack budget_per_timestep=0.0, action_space=None, + _local_dir_cls=None ): if action_space is not None: diff --git a/grid2op/Runner/aux_fun.py b/grid2op/Runner/aux_fun.py index 22c38527b..83ae34cd6 100644 --- a/grid2op/Runner/aux_fun.py +++ b/grid2op/Runner/aux_fun.py @@ -57,8 +57,7 @@ def _aux_one_process_parrallel( for i, ep_id in enumerate(episode_this_process): # `ep_id`: grid2op id of the episode i want to play # `i`: my id of the episode played (0, 1, ... episode_this_process) - env, agent = runner._new_env(parameters=parameters - ) + env, agent = runner._new_env(parameters=parameters) try: env_seed = None if env_seeds is not None: @@ -338,6 +337,7 @@ def _aux_run_one_episode( episode.set_episode_times(env, time_act, beg_, end_) episode.to_disk() + episode.make_serializable() name_chron = env.chronics_handler.get_name() return (name_chron, cum_reward, int(time_step), diff --git a/grid2op/Runner/runner.py b/grid2op/Runner/runner.py index ce6467546..189dbefa6 100644 --- a/grid2op/Runner/runner.py +++ b/grid2op/Runner/runner.py @@ -74,8 +74,12 @@ class Runner(object): env = grid2op.make("l2rpn_case14_sandbox") + # use of a Runner + runner = Runner(**env.get_params_for_runner(), agentClass=RandomAgent) + res = runner.run(nb_episode=nn_episode) + ############### - # the gym loops + # the "equivalent" gym loops nb_episode = 5 for i in range(nb_episode): obs = env.reset() @@ -84,11 +88,10 @@ class Runner(object): while not done: act = agent.act(obs, reward, done) obs, reward, done, info = env.step(act) - + # but this loop does not handle the seeding, does not save the results + # does not store anything related to the run you made etc. + # the Runner can do that with simple calls (see bellow) ############### - # equivalent with use of a Runner - runner = Runner(**env.get_params_for_runner(), agentClass=RandomAgent) - res = runner.run(nb_episode=nn_episode) This specific class as for main purpose to evaluate the performance of a trained @@ -101,6 +104,109 @@ class Runner(object): encourage you to use the :func:`grid2op.Environment.Environment.get_params_for_runner` for creating a runner. + You can customize the agent instance you want with the following code: + + .. code-block:: python + + import grid2op + from grid2op.Agent import RandomAgent # for example... + from grid2op.Runner import Runner + + env = grid2op.make("l2rpn_case14_sandbox") + + agent_instance = RandomAgent(env.action_space) + runner = Runner(**env.get_params_for_runner(), agentClass=None, agentInstance=agent_instance) + res = runner.run(nb_episode=nn_episode) + + You can customize the seeds, the scenarios ID you want, the number of initial steps to skip, the + maximum duration of an episode etc. For more information, please refer to the :func:`Runner.run` + + You can also easily retrieve the :class:`grid2op.Episode.EpisodeData` representing your runs with: + + .. code-block:: python + + import grid2op + from grid2op.Agent import RandomAgent # for example... + from grid2op.Runner import Runner + + env = grid2op.make("l2rpn_case14_sandbox") + + agent_instance = RandomAgent(env.action_space) + runner = Runner(**env.get_params_for_runner(), agentClass=None, agentInstance=agent_instance) + res = runner.run(nb_episode=2, + add_detailed_output=True) + for *_, ep_data in res: + # ep_data are the EpisodeData you can use to do whatever + ... + + You can save the results in a standardized format with: + + .. code-block:: python + + import grid2op + from grid2op.Agent import RandomAgent # for example... + from grid2op.Runner import Runner + + env = grid2op.make("l2rpn_case14_sandbox") + + agent_instance = RandomAgent(env.action_space) + runner = Runner(**env.get_params_for_runner(), agentClass=None, agentInstance=agent_instance) + res = runner.run(nb_episode=2, + save_path="A/PATH/SOMEWHERE") # eg "/home/user/you/grid2op_results/this_run" + + You can also easily (on some platform) easily make the evaluation faster by using the "multi processing" python + package with: + + .. code-block:: python + + import grid2op + from grid2op.Agent import RandomAgent # for example... + from grid2op.Runner import Runner + + env = grid2op.make("l2rpn_case14_sandbox") + + agent_instance = RandomAgent(env.action_space) + runner = Runner(**env.get_params_for_runner(), agentClass=None, agentInstance=agent_instance) + res = runner.run(nb_episode=2, + nb_process=2) + + And, as of grid2op 1.10.3 you can know customize the multi processing context you want + to use to evaluate your agent, like this: + + .. code-block:: python + + import multiprocessing as mp + import grid2op + from grid2op.Agent import RandomAgent # for example... + from grid2op.Runner import Runner + + env = grid2op.make("l2rpn_case14_sandbox") + + agent_instance = RandomAgent(env.action_space) + + ctx = mp.get_context('spawn') # or "fork" or "forkserver" + runner = Runner(**env.get_params_for_runner(), + agentClass=None, + agentInstance=agent_instance, + mp_context=ctx) + res = runner.run(nb_episode=2, + nb_process=2) + + If you set this, the multiprocessing `Pool` used to evaluate your agents will be made with: + + .. code-block:: python + + with mp_context.Pool(nb_process) as p: + .... + + Otherwise the default "Pool" is used: + + .. code-block:: python + + with Pool(nb_process) as p: + .... + + Attributes ---------- envClass: ``type`` @@ -289,10 +395,12 @@ def __init__( kwargs_observation=None, observation_bk_class=None, observation_bk_kwargs=None, - + mp_context=None, # experimental: whether to read from local dir or generate the classes on the fly: - _read_from_local_dir=False, + _read_from_local_dir=None, _is_test=False, # TODO not implemented !! + _local_dir_cls=None, + _overload_name_multimix=None ): """ Initialize the Runner. @@ -360,6 +468,7 @@ def __init__( self._n_busbar = n_busbar self.with_forecast = with_forecast self.name_env = name_env + self._overload_name_multimix = _overload_name_multimix if not isinstance(envClass, type): raise Grid2OpException( 'Parameter "envClass" used to build the Runner should be a type (a class) and not an object ' @@ -375,7 +484,6 @@ def __init__( self.other_env_kwargs = other_env_kwargs else: self.other_env_kwargs = {} - if not isinstance(actionClass, type): raise Grid2OpException( 'Parameter "actionClass" used to build the Runner should be a type (a class) and not an object ' @@ -457,6 +565,14 @@ def __init__( else: self._backend_kwargs = {} + # we keep a reference to the local directory (tmpfile) where + # the classes definition are stored while the runner lives + self._local_dir_cls = _local_dir_cls + + # multi processing context that controls the way the computations are + # distributed when using multiple processes + self._mp_context = mp_context + self.__can_copy_agent = True if agentClass is not None: if agentInstance is not None: @@ -647,8 +763,8 @@ def _new_env(self, parameters) -> Tuple[BaseEnv, BaseAgent]: with warnings.catch_warnings(): warnings.filterwarnings("ignore") res = self.envClass.init_obj_from_kwargs( - n_busbar=self._n_busbar, other_env_kwargs=self.other_env_kwargs, + n_busbar=self._n_busbar, init_env_path=self.init_env_path, init_grid_path=self.init_grid_path, chronics_handler=chronics_handler, @@ -681,6 +797,9 @@ def _new_env(self, parameters) -> Tuple[BaseEnv, BaseAgent]: observation_bk_kwargs=self._observation_bk_kwargs, _raw_backend_class=self.backendClass, _read_from_local_dir=self._read_from_local_dir, + # _local_dir_cls: we don't set it, in parrallel mode it makes no sense ! + _local_dir_cls=None, + _overload_name_multimix=self._overload_name_multimix ) if self.thermal_limit_a is not None: @@ -717,7 +836,7 @@ def reset(self): .. warning:: /!\\\\ Internal, do not use unless you know what you are doing /!\\\\ Used to reset an environment. This method is called at the beginning of each new episode. - If the environment is not initialized, then it initializes it with :func:`Runner.make_env`. + If the environment is not initialized, then it initializes it with :func:`Runner.init_env`. """ pass @@ -1044,6 +1163,8 @@ def _run_parrallel( reset_options=reset_options ) else: + if self._local_dir_cls is not None: + self._local_dir_cls._RUNNER_DO_NOT_ERASE = True self._clean_up() nb_process = int(nb_process) @@ -1105,7 +1226,6 @@ def _run_parrallel( lists = [(self,) for _ in enumerate(process_ids)] else: lists = [(Runner(**self._get_params()),) for _ in enumerate(process_ids)] - for i, pn in enumerate(process_ids): lists[i] = (*lists[i], pn, @@ -1118,14 +1238,17 @@ def _run_parrallel( add_nb_highres_sim, init_states_res[i], reset_options_res[i]) - - if get_start_method() == 'spawn': - # https://github.com/rte-france/Grid2Op/issues/600 - with get_context("spawn").Pool(nb_process) as p: - tmp = p.starmap(_aux_one_process_parrallel, lists) - else: - with Pool(nb_process) as p: + if self._mp_context is not None: + with self._mp_context.Pool(nb_process) as p: tmp = p.starmap(_aux_one_process_parrallel, lists) + else: + if get_start_method() == 'spawn': + # https://github.com/rte-france/Grid2Op/issues/600 + with get_context("spawn").Pool(nb_process) as p: + tmp = p.starmap(_aux_one_process_parrallel, lists) + else: + with Pool(nb_process) as p: + tmp = p.starmap(_aux_one_process_parrallel, lists) for el in tmp: res += el return res @@ -1171,8 +1294,15 @@ def _get_params(self): "logger": self.logger, "use_compact_episode_data": self.use_compact_episode_data, "kwargs_observation": self._kwargs_observation, + "observation_bk_class": self._observation_bk_class, + "observation_bk_kwargs": self._observation_bk_kwargs, "_read_from_local_dir": self._read_from_local_dir, "_is_test": self._is_test, + "_overload_name_multimix": self._overload_name_multimix, + "other_env_kwargs": self.other_env_kwargs, + "n_busbar": self._n_busbar, + "mp_context": None, # this is used in multi processing context, avoid to multi process a multi process stuff + "_local_dir_cls": self._local_dir_cls, } return res diff --git a/grid2op/Space/GridObjects.py b/grid2op/Space/GridObjects.py index 11c87c652..5b8d285b8 100644 --- a/grid2op/Space/GridObjects.py +++ b/grid2op/Space/GridObjects.py @@ -19,7 +19,9 @@ """ import warnings import copy +import os import numpy as np +import sys from packaging import version from typing import Dict, Union, Literal, Any, List, Optional, ClassVar, Tuple @@ -2350,14 +2352,14 @@ def _check_validity_alarm_data(cls): # the "alarm" feature is supported assert isinstance( - cls.alarms_area_names, list - ), "cls.alarms_area_names should be a list" + cls.alarms_area_names, (list, tuple) + ), "cls.alarms_area_names should be a list or a tuple" assert isinstance( cls.alarms_lines_area, dict ), "cls.alarms_lines_area should be a dict" assert isinstance( - cls.alarms_area_lines, list - ), "cls.alarms_area_lines should be a dict" + cls.alarms_area_lines, (list, tuple) + ), "cls.alarms_area_lines should be a list or a tuple" assert ( len(cls.alarms_area_names) == cls.dim_alarms ), "len(cls.alarms_area_names) != cls.dim_alarms" @@ -2876,7 +2878,48 @@ def set_env_name(cls, name): cls.env_name = name @classmethod - def init_grid(cls, gridobj, force=False, extra_name=None, force_module=None): + def _aux_init_grid_from_cls(cls, gridobj, name_res): + import importlib + # NB: these imports needs to be consistent with what is done in + # base_env.generate_classes() + super_module_nm, module_nm = os.path.split(gridobj._PATH_GRID_CLASSES) + if module_nm == "_grid2op_classes": + # legacy "experimental_read_from_local_dir" + # issue was the module "_grid2op_classes" had the same name + # regardless of the environment, so grid2op was "confused" + env_path, env_nm = os.path.split(super_module_nm) + if env_path not in sys.path: + sys.path.append(env_path) + super_supermodule = importlib.import_module(env_nm) + module_nm = f"{env_nm}.{module_nm}" + super_module_nm = super_supermodule + + if f"{module_nm}.{name_res}_file" in sys.modules: + cls_res = getattr(sys.modules[f"{module_nm}.{name_res}_file"], name_res) + # do not forget to create the cls_dict once and for all + if cls_res._CLS_DICT is None: + tmp = {} + cls_res._make_cls_dict_extended(cls_res, tmp, as_list=False) + return cls_res + + super_module = importlib.import_module(module_nm, super_module_nm) # env/path/_grid2op_classes/ + module_all_classes = importlib.import_module(f"{module_nm}") # module specific to the tmpdir created + try: + module = importlib.import_module(f".{name_res}_file", package=module_nm) # module containing the definition of the class + except ModuleNotFoundError: + # in case we need to build the cache again if the module is not found the first time + importlib.invalidate_caches() + importlib.reload(super_module) + module = importlib.import_module(f".{name_res}_file", package=module_nm) + cls_res = getattr(module, name_res) + # do not forget to create the cls_dict once and for all + if cls_res._CLS_DICT is None: + tmp = {} + cls_res._make_cls_dict_extended(cls_res, tmp, as_list=False) + return cls_res + + @classmethod + def init_grid(cls, gridobj, force=False, extra_name=None, force_module=None, _local_dir_cls=None): """ INTERNAL @@ -2923,15 +2966,38 @@ def init_grid(cls, gridobj, force=False, extra_name=None, force_module=None): # to be able to load same environment with # different `n_busbar_per_sub` name_res += f"_{gridobj.n_busbar_per_sub}" + + if _local_dir_cls is not None and gridobj._PATH_GRID_CLASSES is not None: + # new in grid2op 1.10.3: + # if I end up here it's because (done in base_env.generate_classes()): + # 1) the first initial env has already been created + # 2) I need to init the class from the files (and not from whetever else) + # So i do it. And if that is the case, the files are created on the hard drive + # AND the module is added to the path + + # check that it matches (security / consistency check) + if not os.path.samefile(_local_dir_cls.name , gridobj._PATH_GRID_CLASSES): + # in windows the string comparison fails because of things like "/", "\" or "\\" + # this is why we use "samefile" + raise EnvError(f"Unable to create the class: mismatch between " + f"_local_dir_cls ({_local_dir_cls.name}) and " + f" _PATH_GRID_CLASSES ({gridobj._PATH_GRID_CLASSES})") + return cls._aux_init_grid_from_cls(gridobj, name_res) + elif gridobj._PATH_GRID_CLASSES is not None: + # If I end up it's because the environment is created with already initialized + # classes. + return cls._aux_init_grid_from_cls(gridobj, name_res) + # legacy behaviour: build the class "on the fly" + # of new (>= 1.10.3 for the intial creation of the environment) if name_res in globals(): - if not force: + if not force and _local_dir_cls is None: # no need to recreate the class, it already exists return globals()[name_res] else: # i recreate the variable del globals()[name_res] - + cls_attr_as_dict = {} GridObjects._make_cls_dict_extended(gridobj, cls_attr_as_dict, as_list=False) res_cls = type(name_res, (cls,), cls_attr_as_dict) @@ -4107,10 +4173,16 @@ class res(GridObjects): cls.glop_version = cls.BEFORE_COMPAT_VERSION if "_PATH_GRID_CLASSES" in dict_: - cls._PATH_GRID_CLASSES = str(dict_["_PATH_GRID_CLASSES"]) + if dict_["_PATH_GRID_CLASSES"] is not None: + cls._PATH_GRID_CLASSES = str(dict_["_PATH_GRID_CLASSES"]) + else: + cls._PATH_GRID_CLASSES = None elif "_PATH_ENV" in dict_: # legacy mode in grid2op <= 1.10.1 this was saved in "PATH_ENV" - cls._PATH_GRID_CLASSES = str(dict_["_PATH_ENV"]) + if dict_["_PATH_ENV"] is not None: + cls._PATH_GRID_CLASSES = str(dict_["_PATH_ENV"]) + else: + cls._PATH_GRID_CLASSES = None else: cls._PATH_GRID_CLASSES = None @@ -4867,11 +4939,11 @@ class {cls.__name__}({cls._INIT_GRID_CLS.__name__}): # name of the objects env_name = "{cls.env_name}" - name_load = np.array([{name_load_str}]) - name_gen = np.array([{name_gen_str}]) - name_line = np.array([{name_line_str}]) - name_sub = np.array([{name_sub_str}]) - name_storage = np.array([{name_storage_str}]) + name_load = np.array([{name_load_str}], dtype=str) + name_gen = np.array([{name_gen_str}], dtype=str) + name_line = np.array([{name_line_str}], dtype=str) + name_sub = np.array([{name_sub_str}], dtype=str) + name_storage = np.array([{name_storage_str}], dtype=str) n_busbar_per_sub = {cls.n_busbar_per_sub} n_gen = {cls.n_gen} @@ -4935,7 +5007,7 @@ class {cls.__name__}({cls._INIT_GRID_CLS.__name__}): gen_renewable = {gen_renewable_str} # storage unit static data - storage_type = np.array([{storage_type_str}]) + storage_type = np.array([{storage_type_str}], dtype=str) storage_Emax = {storage_Emax_str} storage_Emin = {storage_Emin_str} storage_max_p_prod = {storage_max_p_prod_str} @@ -4965,7 +5037,7 @@ class {cls.__name__}({cls._INIT_GRID_CLS.__name__}): alarms_area_lines = {alarms_area_lines_str} # alert feature - dim_alert = {cls.dim_alerts} + dim_alerts = {cls.dim_alerts} alertable_line_names = {alertable_line_names_str} alertable_line_ids = {alertable_line_ids_str} diff --git a/grid2op/Space/SerializableSpace.py b/grid2op/Space/SerializableSpace.py index a19a57b5a..379743169 100644 --- a/grid2op/Space/SerializableSpace.py +++ b/grid2op/Space/SerializableSpace.py @@ -61,7 +61,7 @@ class SerializableSpace(GridObjects, RandomObject): """ - def __init__(self, gridobj, subtype=object, _init_grid=True): + def __init__(self, gridobj, subtype=object, _init_grid=True, _local_dir_cls=None): """ subtype: ``type`` @@ -83,7 +83,7 @@ def __init__(self, gridobj, subtype=object, _init_grid=True): RandomObject.__init__(self) self._init_subtype = subtype # do not use, use to save restore only !!! if _init_grid: - self.subtype = subtype.init_grid(gridobj) + self.subtype = subtype.init_grid(gridobj, _local_dir_cls=_local_dir_cls) from grid2op.Action import ( BaseAction, ) # lazy loading to prevent circular reference @@ -185,7 +185,8 @@ def from_dict(dict_): gridobj = GridObjects.from_dict(dict_) actionClass_str = extract_from_dict(dict_, "_init_subtype", str) actionClass_li = actionClass_str.split(".") - + _local_dir_cls = None # TODO when reading back the data + if actionClass_li[-1] in globals(): subtype = globals()[actionClass_li[-1]] else: @@ -265,8 +266,8 @@ def from_dict(dict_): msg_err_ = msg_err_.format(actionClass_str) raise Grid2OpException(msg_err_) # create the proper SerializableSpace class for this environment - CLS = SerializableSpace.init_grid(gridobj) - res = CLS(gridobj=gridobj, subtype=subtype, _init_grid=True) + CLS = SerializableSpace.init_grid(gridobj, _local_dir_cls=_local_dir_cls) + res = CLS(gridobj=gridobj, subtype=subtype, _init_grid=True, _local_dir_cls=_local_dir_cls) return res def cls_to_dict(self): diff --git a/grid2op/VoltageControler/BaseVoltageController.py b/grid2op/VoltageControler/BaseVoltageController.py index 02eb6c978..e29fc883f 100644 --- a/grid2op/VoltageControler/BaseVoltageController.py +++ b/grid2op/VoltageControler/BaseVoltageController.py @@ -23,7 +23,7 @@ class BaseVoltageController(RandomObject, ABC): If the voltages are not on the chronics (missing files), it will not change the voltage setpoints at all. """ - def __init__(self, gridobj, controler_backend, actionSpace_cls): + def __init__(self, gridobj, controler_backend, actionSpace_cls, _local_dir_cls=None): """ Parameters @@ -39,7 +39,10 @@ def __init__(self, gridobj, controler_backend, actionSpace_cls): legal_act = AlwaysLegal() self._actionSpace_cls = actionSpace_cls self.action_space = actionSpace_cls( - gridobj=gridobj, actionClass=VoltageOnlyAction, legal_action=legal_act + gridobj=gridobj, + actionClass=VoltageOnlyAction, + legal_action=legal_act, + _local_dir_cls=_local_dir_cls ) def _custom_deepcopy_for_copy(self, new_obj): diff --git a/grid2op/VoltageControler/ControlVoltageFromFile.py b/grid2op/VoltageControler/ControlVoltageFromFile.py index ed6004842..3322eafe0 100644 --- a/grid2op/VoltageControler/ControlVoltageFromFile.py +++ b/grid2op/VoltageControler/ControlVoltageFromFile.py @@ -19,7 +19,11 @@ class ControlVoltageFromFile(BaseVoltageController): If the voltages are not on the chronics (missing files), it will not change the voltage setpoint at all. """ - def __init__(self, gridobj, controler_backend, actionSpace_cls): + def __init__(self, + gridobj, + controler_backend, + actionSpace_cls, + _local_dir_cls=None): """ Parameters @@ -36,6 +40,7 @@ def __init__(self, gridobj, controler_backend, actionSpace_cls): gridobj=gridobj, controler_backend=controler_backend, actionSpace_cls=actionSpace_cls, + _local_dir_cls=_local_dir_cls ) def fix_voltage(self, observation, agent_action, env_action, prod_v_chronics): diff --git a/grid2op/data/educ_case14_redisp/__init__.py b/grid2op/data/educ_case14_redisp/__init__.py new file mode 100644 index 000000000..bd6582d7e --- /dev/null +++ b/grid2op/data/educ_case14_redisp/__init__.py @@ -0,0 +1 @@ +# DO NOT REMOVE, automatically generated by grid2op \ No newline at end of file diff --git a/grid2op/data/educ_case14_storage/__init__.py b/grid2op/data/educ_case14_storage/__init__.py new file mode 100644 index 000000000..bd6582d7e --- /dev/null +++ b/grid2op/data/educ_case14_storage/__init__.py @@ -0,0 +1 @@ +# DO NOT REMOVE, automatically generated by grid2op \ No newline at end of file diff --git a/grid2op/data/l2rpn_case14_sandbox/__init__.py b/grid2op/data/l2rpn_case14_sandbox/__init__.py new file mode 100644 index 000000000..bd6582d7e --- /dev/null +++ b/grid2op/data/l2rpn_case14_sandbox/__init__.py @@ -0,0 +1 @@ +# DO NOT REMOVE, automatically generated by grid2op \ No newline at end of file diff --git a/grid2op/data/l2rpn_case14_sandbox_diff_grid/__init__.py b/grid2op/data/l2rpn_case14_sandbox_diff_grid/__init__.py new file mode 100644 index 000000000..bd6582d7e --- /dev/null +++ b/grid2op/data/l2rpn_case14_sandbox_diff_grid/__init__.py @@ -0,0 +1 @@ +# DO NOT REMOVE, automatically generated by grid2op \ No newline at end of file diff --git a/grid2op/data/l2rpn_icaps_2021/__init__.py b/grid2op/data/l2rpn_icaps_2021/__init__.py new file mode 100644 index 000000000..bd6582d7e --- /dev/null +++ b/grid2op/data/l2rpn_icaps_2021/__init__.py @@ -0,0 +1 @@ +# DO NOT REMOVE, automatically generated by grid2op \ No newline at end of file diff --git a/grid2op/data/l2rpn_idf_2023/__init__.py b/grid2op/data/l2rpn_idf_2023/__init__.py new file mode 100644 index 000000000..bd6582d7e --- /dev/null +++ b/grid2op/data/l2rpn_idf_2023/__init__.py @@ -0,0 +1 @@ +# DO NOT REMOVE, automatically generated by grid2op \ No newline at end of file diff --git a/grid2op/data/l2rpn_neurips_2020_track1/__init__.py b/grid2op/data/l2rpn_neurips_2020_track1/__init__.py new file mode 100644 index 000000000..bd6582d7e --- /dev/null +++ b/grid2op/data/l2rpn_neurips_2020_track1/__init__.py @@ -0,0 +1 @@ +# DO NOT REMOVE, automatically generated by grid2op \ No newline at end of file diff --git a/grid2op/data/l2rpn_neurips_2020_track2/x1/__init__.py b/grid2op/data/l2rpn_neurips_2020_track2/x1/__init__.py new file mode 100644 index 000000000..bd6582d7e --- /dev/null +++ b/grid2op/data/l2rpn_neurips_2020_track2/x1/__init__.py @@ -0,0 +1 @@ +# DO NOT REMOVE, automatically generated by grid2op \ No newline at end of file diff --git a/grid2op/data/l2rpn_neurips_2020_track2/x2.5/__init__.py b/grid2op/data/l2rpn_neurips_2020_track2/x2.5/__init__.py new file mode 100644 index 000000000..bd6582d7e --- /dev/null +++ b/grid2op/data/l2rpn_neurips_2020_track2/x2.5/__init__.py @@ -0,0 +1 @@ +# DO NOT REMOVE, automatically generated by grid2op \ No newline at end of file diff --git a/grid2op/data/l2rpn_wcci_2020/__init__.py b/grid2op/data/l2rpn_wcci_2020/__init__.py new file mode 100644 index 000000000..bd6582d7e --- /dev/null +++ b/grid2op/data/l2rpn_wcci_2020/__init__.py @@ -0,0 +1 @@ +# DO NOT REMOVE, automatically generated by grid2op \ No newline at end of file diff --git a/grid2op/data/l2rpn_wcci_2022_dev/__init__.py b/grid2op/data/l2rpn_wcci_2022_dev/__init__.py new file mode 100644 index 000000000..bd6582d7e --- /dev/null +++ b/grid2op/data/l2rpn_wcci_2022_dev/__init__.py @@ -0,0 +1 @@ +# DO NOT REMOVE, automatically generated by grid2op \ No newline at end of file diff --git a/grid2op/data/rte_case118_example/__init__.py b/grid2op/data/rte_case118_example/__init__.py new file mode 100644 index 000000000..bd6582d7e --- /dev/null +++ b/grid2op/data/rte_case118_example/__init__.py @@ -0,0 +1 @@ +# DO NOT REMOVE, automatically generated by grid2op \ No newline at end of file diff --git a/grid2op/data/rte_case14_opponent/__init__.py b/grid2op/data/rte_case14_opponent/__init__.py new file mode 100644 index 000000000..bd6582d7e --- /dev/null +++ b/grid2op/data/rte_case14_opponent/__init__.py @@ -0,0 +1 @@ +# DO NOT REMOVE, automatically generated by grid2op \ No newline at end of file diff --git a/grid2op/data/rte_case14_realistic/__init__.py b/grid2op/data/rte_case14_realistic/__init__.py new file mode 100644 index 000000000..bd6582d7e --- /dev/null +++ b/grid2op/data/rte_case14_realistic/__init__.py @@ -0,0 +1 @@ +# DO NOT REMOVE, automatically generated by grid2op \ No newline at end of file diff --git a/grid2op/data/rte_case14_redisp/__init__.py b/grid2op/data/rte_case14_redisp/__init__.py new file mode 100644 index 000000000..bd6582d7e --- /dev/null +++ b/grid2op/data/rte_case14_redisp/__init__.py @@ -0,0 +1 @@ +# DO NOT REMOVE, automatically generated by grid2op \ No newline at end of file diff --git a/grid2op/data/rte_case14_test/__init__.py b/grid2op/data/rte_case14_test/__init__.py new file mode 100644 index 000000000..bd6582d7e --- /dev/null +++ b/grid2op/data/rte_case14_test/__init__.py @@ -0,0 +1 @@ +# DO NOT REMOVE, automatically generated by grid2op \ No newline at end of file diff --git a/grid2op/data/rte_case5_example/__init__.py b/grid2op/data/rte_case5_example/__init__.py new file mode 100644 index 000000000..bd6582d7e --- /dev/null +++ b/grid2op/data/rte_case5_example/__init__.py @@ -0,0 +1 @@ +# DO NOT REMOVE, automatically generated by grid2op \ No newline at end of file diff --git a/grid2op/data_test/l2rpn_idf_2023_with_alert/__init__.py b/grid2op/data_test/l2rpn_idf_2023_with_alert/__init__.py new file mode 100644 index 000000000..bd6582d7e --- /dev/null +++ b/grid2op/data_test/l2rpn_idf_2023_with_alert/__init__.py @@ -0,0 +1 @@ +# DO NOT REMOVE, automatically generated by grid2op \ No newline at end of file diff --git a/grid2op/data_test/multimix/case14_002/chronics/0/hazards.csv.bz2 b/grid2op/data_test/multimix/case14_002/chronics/0/hazards.csv.bz2 new file mode 100644 index 000000000..19f4c400c Binary files /dev/null and b/grid2op/data_test/multimix/case14_002/chronics/0/hazards.csv.bz2 differ diff --git a/grid2op/data_test/multimix/case14_002/chronics/0/load_p.csv.bz2 b/grid2op/data_test/multimix/case14_002/chronics/0/load_p.csv.bz2 new file mode 100644 index 000000000..5e58c54d6 Binary files /dev/null and b/grid2op/data_test/multimix/case14_002/chronics/0/load_p.csv.bz2 differ diff --git a/grid2op/data_test/multimix/case14_002/chronics/0/load_p_forecasted.csv.bz2 b/grid2op/data_test/multimix/case14_002/chronics/0/load_p_forecasted.csv.bz2 new file mode 100644 index 000000000..afa02b2b1 Binary files /dev/null and b/grid2op/data_test/multimix/case14_002/chronics/0/load_p_forecasted.csv.bz2 differ diff --git a/grid2op/data_test/multimix/case14_002/chronics/0/load_q.csv.bz2 b/grid2op/data_test/multimix/case14_002/chronics/0/load_q.csv.bz2 new file mode 100644 index 000000000..335611c49 Binary files /dev/null and b/grid2op/data_test/multimix/case14_002/chronics/0/load_q.csv.bz2 differ diff --git a/grid2op/data_test/multimix/case14_002/chronics/0/load_q_forecasted.csv.bz2 b/grid2op/data_test/multimix/case14_002/chronics/0/load_q_forecasted.csv.bz2 new file mode 100644 index 000000000..3183c02d1 Binary files /dev/null and b/grid2op/data_test/multimix/case14_002/chronics/0/load_q_forecasted.csv.bz2 differ diff --git a/grid2op/data_test/multimix/case14_002/chronics/0/maintenance.csv.bz2 b/grid2op/data_test/multimix/case14_002/chronics/0/maintenance.csv.bz2 new file mode 100644 index 000000000..19f4c400c Binary files /dev/null and b/grid2op/data_test/multimix/case14_002/chronics/0/maintenance.csv.bz2 differ diff --git a/grid2op/data_test/multimix/case14_002/chronics/0/maintenance_forecasted.csv.bz2 b/grid2op/data_test/multimix/case14_002/chronics/0/maintenance_forecasted.csv.bz2 new file mode 100644 index 000000000..19f4c400c Binary files /dev/null and b/grid2op/data_test/multimix/case14_002/chronics/0/maintenance_forecasted.csv.bz2 differ diff --git a/grid2op/data_test/multimix/case14_002/chronics/0/prod_p.csv.bz2 b/grid2op/data_test/multimix/case14_002/chronics/0/prod_p.csv.bz2 new file mode 100644 index 000000000..b523af8b6 Binary files /dev/null and b/grid2op/data_test/multimix/case14_002/chronics/0/prod_p.csv.bz2 differ diff --git a/grid2op/data_test/multimix/case14_002/chronics/0/prod_p_forecasted.csv.bz2 b/grid2op/data_test/multimix/case14_002/chronics/0/prod_p_forecasted.csv.bz2 new file mode 100644 index 000000000..7ee0bdb2b Binary files /dev/null and b/grid2op/data_test/multimix/case14_002/chronics/0/prod_p_forecasted.csv.bz2 differ diff --git a/grid2op/data_test/multimix/case14_002/chronics/0/prod_v.csv.bz2 b/grid2op/data_test/multimix/case14_002/chronics/0/prod_v.csv.bz2 new file mode 100644 index 000000000..2d590080e Binary files /dev/null and b/grid2op/data_test/multimix/case14_002/chronics/0/prod_v.csv.bz2 differ diff --git a/grid2op/data_test/multimix/case14_002/chronics/0/prod_v_forecasted.csv.bz2 b/grid2op/data_test/multimix/case14_002/chronics/0/prod_v_forecasted.csv.bz2 new file mode 100644 index 000000000..b7c0d91cc Binary files /dev/null and b/grid2op/data_test/multimix/case14_002/chronics/0/prod_v_forecasted.csv.bz2 differ diff --git a/grid2op/data_test/multimix/case14_002/chronics/000/start_datetime.info b/grid2op/data_test/multimix/case14_002/chronics/0/start_datetime.info similarity index 100% rename from grid2op/data_test/multimix/case14_002/chronics/000/start_datetime.info rename to grid2op/data_test/multimix/case14_002/chronics/0/start_datetime.info diff --git a/grid2op/data_test/multimix/case14_002/chronics/000/time_interval.info b/grid2op/data_test/multimix/case14_002/chronics/0/time_interval.info similarity index 100% rename from grid2op/data_test/multimix/case14_002/chronics/000/time_interval.info rename to grid2op/data_test/multimix/case14_002/chronics/0/time_interval.info diff --git a/grid2op/data_test/multimix/case14_002/chronics/000/load_p.csv.bz2 b/grid2op/data_test/multimix/case14_002/chronics/000/load_p.csv.bz2 deleted file mode 100644 index 77fd7af71..000000000 Binary files a/grid2op/data_test/multimix/case14_002/chronics/000/load_p.csv.bz2 and /dev/null differ diff --git a/grid2op/data_test/multimix/case14_002/chronics/000/load_p_forecasted.csv.bz2 b/grid2op/data_test/multimix/case14_002/chronics/000/load_p_forecasted.csv.bz2 deleted file mode 100644 index ce08ec0e1..000000000 Binary files a/grid2op/data_test/multimix/case14_002/chronics/000/load_p_forecasted.csv.bz2 and /dev/null differ diff --git a/grid2op/data_test/multimix/case14_002/chronics/000/load_q.csv.bz2 b/grid2op/data_test/multimix/case14_002/chronics/000/load_q.csv.bz2 deleted file mode 100644 index b2b092db7..000000000 Binary files a/grid2op/data_test/multimix/case14_002/chronics/000/load_q.csv.bz2 and /dev/null differ diff --git a/grid2op/data_test/multimix/case14_002/chronics/000/load_q_forecasted.csv.bz2 b/grid2op/data_test/multimix/case14_002/chronics/000/load_q_forecasted.csv.bz2 deleted file mode 100644 index 631f0f40b..000000000 Binary files a/grid2op/data_test/multimix/case14_002/chronics/000/load_q_forecasted.csv.bz2 and /dev/null differ diff --git a/grid2op/data_test/multimix/case14_002/chronics/000/prod_p.csv.bz2 b/grid2op/data_test/multimix/case14_002/chronics/000/prod_p.csv.bz2 deleted file mode 100644 index 84bf12179..000000000 Binary files a/grid2op/data_test/multimix/case14_002/chronics/000/prod_p.csv.bz2 and /dev/null differ diff --git a/grid2op/data_test/multimix/case14_002/chronics/000/prod_p_forecasted.csv.bz2 b/grid2op/data_test/multimix/case14_002/chronics/000/prod_p_forecasted.csv.bz2 deleted file mode 100644 index 2d7ef6442..000000000 Binary files a/grid2op/data_test/multimix/case14_002/chronics/000/prod_p_forecasted.csv.bz2 and /dev/null differ diff --git a/grid2op/data_test/multimix/case14_002/chronics/000/prod_v.csv.bz2 b/grid2op/data_test/multimix/case14_002/chronics/000/prod_v.csv.bz2 deleted file mode 100644 index c300e1563..000000000 Binary files a/grid2op/data_test/multimix/case14_002/chronics/000/prod_v.csv.bz2 and /dev/null differ diff --git a/grid2op/data_test/multimix/case14_002/chronics/000/prod_v_forecasted.csv.bz2 b/grid2op/data_test/multimix/case14_002/chronics/000/prod_v_forecasted.csv.bz2 deleted file mode 100644 index 70cb99dbc..000000000 Binary files a/grid2op/data_test/multimix/case14_002/chronics/000/prod_v_forecasted.csv.bz2 and /dev/null differ diff --git a/grid2op/data_test/multimix/case14_002/chronics/001/load_p.csv.bz2 b/grid2op/data_test/multimix/case14_002/chronics/001/load_p.csv.bz2 deleted file mode 100644 index 30336696c..000000000 Binary files a/grid2op/data_test/multimix/case14_002/chronics/001/load_p.csv.bz2 and /dev/null differ diff --git a/grid2op/data_test/multimix/case14_002/chronics/001/load_p_forecasted.csv.bz2 b/grid2op/data_test/multimix/case14_002/chronics/001/load_p_forecasted.csv.bz2 deleted file mode 100644 index 5de1d99be..000000000 Binary files a/grid2op/data_test/multimix/case14_002/chronics/001/load_p_forecasted.csv.bz2 and /dev/null differ diff --git a/grid2op/data_test/multimix/case14_002/chronics/001/load_q.csv.bz2 b/grid2op/data_test/multimix/case14_002/chronics/001/load_q.csv.bz2 deleted file mode 100644 index 17d69b9a6..000000000 Binary files a/grid2op/data_test/multimix/case14_002/chronics/001/load_q.csv.bz2 and /dev/null differ diff --git a/grid2op/data_test/multimix/case14_002/chronics/001/load_q_forecasted.csv.bz2 b/grid2op/data_test/multimix/case14_002/chronics/001/load_q_forecasted.csv.bz2 deleted file mode 100644 index 303dbf42d..000000000 Binary files a/grid2op/data_test/multimix/case14_002/chronics/001/load_q_forecasted.csv.bz2 and /dev/null differ diff --git a/grid2op/data_test/multimix/case14_002/chronics/001/prod_p.csv.bz2 b/grid2op/data_test/multimix/case14_002/chronics/001/prod_p.csv.bz2 deleted file mode 100644 index 2a1cf249d..000000000 Binary files a/grid2op/data_test/multimix/case14_002/chronics/001/prod_p.csv.bz2 and /dev/null differ diff --git a/grid2op/data_test/multimix/case14_002/chronics/001/prod_p_forecasted.csv.bz2 b/grid2op/data_test/multimix/case14_002/chronics/001/prod_p_forecasted.csv.bz2 deleted file mode 100644 index c7bc25425..000000000 Binary files a/grid2op/data_test/multimix/case14_002/chronics/001/prod_p_forecasted.csv.bz2 and /dev/null differ diff --git a/grid2op/data_test/multimix/case14_002/chronics/001/prod_v.csv.bz2 b/grid2op/data_test/multimix/case14_002/chronics/001/prod_v.csv.bz2 deleted file mode 100644 index c300e1563..000000000 Binary files a/grid2op/data_test/multimix/case14_002/chronics/001/prod_v.csv.bz2 and /dev/null differ diff --git a/grid2op/data_test/multimix/case14_002/chronics/001/prod_v_forecasted.csv.bz2 b/grid2op/data_test/multimix/case14_002/chronics/001/prod_v_forecasted.csv.bz2 deleted file mode 100644 index 70cb99dbc..000000000 Binary files a/grid2op/data_test/multimix/case14_002/chronics/001/prod_v_forecasted.csv.bz2 and /dev/null differ diff --git a/grid2op/data_test/multimix/case14_002/chronics/1/hazards.csv.bz2 b/grid2op/data_test/multimix/case14_002/chronics/1/hazards.csv.bz2 new file mode 100644 index 000000000..19f4c400c Binary files /dev/null and b/grid2op/data_test/multimix/case14_002/chronics/1/hazards.csv.bz2 differ diff --git a/grid2op/data_test/multimix/case14_002/chronics/1/load_p.csv.bz2 b/grid2op/data_test/multimix/case14_002/chronics/1/load_p.csv.bz2 new file mode 100644 index 000000000..1fb2cfedb Binary files /dev/null and b/grid2op/data_test/multimix/case14_002/chronics/1/load_p.csv.bz2 differ diff --git a/grid2op/data_test/multimix/case14_002/chronics/1/load_p_forecasted.csv.bz2 b/grid2op/data_test/multimix/case14_002/chronics/1/load_p_forecasted.csv.bz2 new file mode 100644 index 000000000..6ec178d02 Binary files /dev/null and b/grid2op/data_test/multimix/case14_002/chronics/1/load_p_forecasted.csv.bz2 differ diff --git a/grid2op/data_test/multimix/case14_002/chronics/1/load_q.csv.bz2 b/grid2op/data_test/multimix/case14_002/chronics/1/load_q.csv.bz2 new file mode 100644 index 000000000..f398706f6 Binary files /dev/null and b/grid2op/data_test/multimix/case14_002/chronics/1/load_q.csv.bz2 differ diff --git a/grid2op/data_test/multimix/case14_002/chronics/1/load_q_forecasted.csv.bz2 b/grid2op/data_test/multimix/case14_002/chronics/1/load_q_forecasted.csv.bz2 new file mode 100644 index 000000000..8deb04b51 Binary files /dev/null and b/grid2op/data_test/multimix/case14_002/chronics/1/load_q_forecasted.csv.bz2 differ diff --git a/grid2op/data_test/multimix/case14_002/chronics/1/maintenance.csv.bz2 b/grid2op/data_test/multimix/case14_002/chronics/1/maintenance.csv.bz2 new file mode 100644 index 000000000..19f4c400c Binary files /dev/null and b/grid2op/data_test/multimix/case14_002/chronics/1/maintenance.csv.bz2 differ diff --git a/grid2op/data_test/multimix/case14_002/chronics/1/maintenance_forecasted.csv.bz2 b/grid2op/data_test/multimix/case14_002/chronics/1/maintenance_forecasted.csv.bz2 new file mode 100644 index 000000000..19f4c400c Binary files /dev/null and b/grid2op/data_test/multimix/case14_002/chronics/1/maintenance_forecasted.csv.bz2 differ diff --git a/grid2op/data_test/multimix/case14_002/chronics/1/prod_p.csv.bz2 b/grid2op/data_test/multimix/case14_002/chronics/1/prod_p.csv.bz2 new file mode 100644 index 000000000..c13834eb5 Binary files /dev/null and b/grid2op/data_test/multimix/case14_002/chronics/1/prod_p.csv.bz2 differ diff --git a/grid2op/data_test/multimix/case14_002/chronics/1/prod_p_forecasted.csv.bz2 b/grid2op/data_test/multimix/case14_002/chronics/1/prod_p_forecasted.csv.bz2 new file mode 100644 index 000000000..6fed9d123 Binary files /dev/null and b/grid2op/data_test/multimix/case14_002/chronics/1/prod_p_forecasted.csv.bz2 differ diff --git a/grid2op/data_test/multimix/case14_002/chronics/1/prod_v.csv.bz2 b/grid2op/data_test/multimix/case14_002/chronics/1/prod_v.csv.bz2 new file mode 100644 index 000000000..2d590080e Binary files /dev/null and b/grid2op/data_test/multimix/case14_002/chronics/1/prod_v.csv.bz2 differ diff --git a/grid2op/data_test/multimix/case14_002/chronics/1/prod_v_forecasted.csv.bz2 b/grid2op/data_test/multimix/case14_002/chronics/1/prod_v_forecasted.csv.bz2 new file mode 100644 index 000000000..b7c0d91cc Binary files /dev/null and b/grid2op/data_test/multimix/case14_002/chronics/1/prod_v_forecasted.csv.bz2 differ diff --git a/grid2op/data_test/multimix/case14_002/chronics/001/start_datetime.info b/grid2op/data_test/multimix/case14_002/chronics/1/start_datetime.info similarity index 100% rename from grid2op/data_test/multimix/case14_002/chronics/001/start_datetime.info rename to grid2op/data_test/multimix/case14_002/chronics/1/start_datetime.info diff --git a/grid2op/data_test/multimix/case14_002/chronics/001/time_interval.info b/grid2op/data_test/multimix/case14_002/chronics/1/time_interval.info similarity index 100% rename from grid2op/data_test/multimix/case14_002/chronics/001/time_interval.info rename to grid2op/data_test/multimix/case14_002/chronics/1/time_interval.info diff --git a/grid2op/data_test/multimix/case14_002/config.py b/grid2op/data_test/multimix/case14_002/config.py index d2e6e585c..1c34314d6 100644 --- a/grid2op/data_test/multimix/case14_002/config.py +++ b/grid2op/data_test/multimix/case14_002/config.py @@ -15,26 +15,26 @@ "grid_value_class": GridStateFromFileWithForecasts, "volagecontroler_class": None, "thermal_limits": [ - 384.900179, - 384.900179, - 380.0, - 380.0, - 157.0, - 380.0, - 380.0, - 1077.7205012, - 461.8802148, - 769.80036, - 269.4301253, - 384.900179, - 760.0, - 380.0, - 760.0, - 384.900179, - 230.9401074, - 170.79945452, - 3402.24266, - 3402.24266, + 3.84900179e02, + 3.84900179e02, + 2.28997102e05, + 2.28997102e05, + 2.28997102e05, + 1.52664735e04, + 2.28997102e05, + 3.84900179e02, + 3.84900179e02, + 1.83285800e02, + 3.84900179e02, + 3.84900179e02, + 2.28997102e05, + 2.28997102e05, + 6.93930612e04, + 3.84900179e02, + 3.84900179e02, + 2.40562612e02, + 3.40224266e03, + 3.40224266e03, ], "names_chronics_to_grid": None, } diff --git a/grid2op/data_test/multimix/case14_002/grid.json b/grid2op/data_test/multimix/case14_002/grid.json index 88699329a..27dacefd7 100644 --- a/grid2op/data_test/multimix/case14_002/grid.json +++ b/grid2op/data_test/multimix/case14_002/grid.json @@ -1,1363 +1,5 @@ { - "_module": "pandapower.auxiliary", - "_class": "pandapowerNet", - "_object": { - "bus": { - "_module": "pandas.core.frame", - "_class": "DataFrame", - "_object": "{\"columns\":[\"name\",\"vn_kv\",\"type\",\"zone\",\"in_service\",\"min_vm_pu\",\"max_vm_pu\"],\"index\":[0,1,2,3,4,5,6,7,8,9,10,11,12,13],\"data\":[[1,138.0,\"b\",1.0,true,0.94,1.06],[2,138.0,\"b\",1.0,true,0.94,1.06],[3,138.0,\"b\",1.0,true,0.94,1.06],[4,138.0,\"b\",1.0,true,0.94,1.06],[5,138.0,\"b\",1.0,true,0.94,1.06],[6,20.0,\"b\",1.0,true,0.94,1.06],[7,14.0,\"b\",1.0,true,0.94,1.06],[8,12.0,\"b\",1.0,true,0.94,1.06],[9,20.0,\"b\",1.0,true,0.94,1.06],[10,20.0,\"b\",1.0,true,0.94,1.06],[11,20.0,\"b\",1.0,true,0.94,1.06],[12,20.0,\"b\",1.0,true,0.94,1.06],[13,20.0,\"b\",1.0,true,0.94,1.06],[14,20.0,\"b\",1.0,true,0.94,1.06]]}", - "orient": "split", - "dtype": { - "name": "object", - "vn_kv": "float64", - "type": "object", - "zone": "object", - "in_service": "bool", - "min_vm_pu": "float64", - "max_vm_pu": "float64" - } - }, - "load": { - "_module": "pandas.core.frame", - "_class": "DataFrame", - "_object": "{\"columns\":[\"name\",\"bus\",\"p_mw\",\"q_mvar\",\"const_z_percent\",\"const_i_percent\",\"sn_mva\",\"scaling\",\"in_service\",\"type\",\"controllable\"],\"index\":[0,1,2,3,4,5,6,7,8,9,10],\"data\":[[null,1,21.699999999999999,12.699999999999999,0.0,0.0,null,1.0,true,null,false],[null,2,94.200000000000003,19.0,0.0,0.0,null,1.0,true,null,false],[null,3,47.799999999999997,-3.9,0.0,0.0,null,1.0,true,null,false],[null,4,7.6,1.6,0.0,0.0,null,1.0,true,null,false],[null,5,11.199999999999999,7.5,0.0,0.0,null,1.0,true,null,false],[null,8,29.5,16.600000000000001,0.0,0.0,null,1.0,true,null,false],[null,9,9.0,5.8,0.0,0.0,null,1.0,true,null,false],[null,10,3.5,1.8,0.0,0.0,null,1.0,true,null,false],[null,11,6.1,1.6,0.0,0.0,null,1.0,true,null,false],[null,12,13.5,5.8,0.0,0.0,null,1.0,true,null,false],[null,13,14.9,5.0,0.0,0.0,null,1.0,true,null,false]]}", - "orient": "split", - "dtype": { - "name": "object", - "bus": "uint32", - "p_mw": "float64", - "q_mvar": "float64", - "const_z_percent": "float64", - "const_i_percent": "float64", - "sn_mva": "float64", - "scaling": "float64", - "in_service": "bool", - "type": "object", - "controllable": "object" - } - }, - "sgen": { - "_module": "pandas.core.frame", - "_class": "DataFrame", - "_object": "{\"columns\":[\"name\",\"bus\",\"p_mw\",\"q_mvar\",\"sn_mva\",\"scaling\",\"in_service\",\"type\",\"current_source\"],\"index\":[],\"data\":[]}", - "orient": "split", - "dtype": { - "name": "object", - "bus": "int64", - "p_mw": "float64", - "q_mvar": "float64", - "sn_mva": "float64", - "scaling": "float64", - "in_service": "bool", - "type": "object", - "current_source": "bool" - } - }, - "storage": { - "_module": "pandas.core.frame", - "_class": "DataFrame", - "_object": "{\"columns\":[\"name\",\"bus\",\"p_mw\",\"q_mvar\",\"sn_mva\",\"soc_percent\",\"min_e_mwh\",\"max_e_mwh\",\"scaling\",\"in_service\",\"type\"],\"index\":[],\"data\":[]}", - "orient": "split", - "dtype": { - "name": "object", - "bus": "int64", - "p_mw": "float64", - "q_mvar": "float64", - "sn_mva": "float64", - "soc_percent": "float64", - "min_e_mwh": "float64", - "max_e_mwh": "float64", - "scaling": "float64", - "in_service": "bool", - "type": "object" - } - }, - "gen": { - "_module": "pandas.core.frame", - "_class": "DataFrame", - "_object": "{\"columns\":[\"name\",\"bus\",\"p_mw\",\"vm_pu\",\"sn_mva\",\"min_q_mvar\",\"max_q_mvar\",\"scaling\",\"slack\",\"in_service\",\"type\",\"controllable\",\"min_p_mw\",\"max_p_mw\"],\"index\":[0,1,2,3],\"data\":[[null,1,40.0,1.045,null,-40.0,50.0,1.0,false,true,null,true,0.0,140.0],[null,2,0.0,1.01,null,0.0,40.0,1.0,false,true,null,true,0.0,100.0],[null,5,0.0,1.07,null,-6.0,24.0,1.0,false,true,null,true,0.0,100.0],[null,7,0.0,1.09,null,-6.0,24.0,1.0,false,true,null,true,0.0,100.0]]}", - "orient": "split", - "dtype": { - "name": "object", - "bus": "uint32", - "p_mw": "float64", - "vm_pu": "float64", - "sn_mva": "float64", - "min_q_mvar": "float64", - "max_q_mvar": "float64", - "scaling": "float64", - "slack": "bool", - "in_service": "bool", - "type": "object", - "controllable": "bool", - "min_p_mw": "float64", - "max_p_mw": "float64" - } - }, - "switch": { - "_module": "pandas.core.frame", - "_class": "DataFrame", - "_object": "{\"columns\":[\"bus\",\"element\",\"et\",\"type\",\"closed\",\"name\",\"z_ohm\"],\"index\":[],\"data\":[]}", - "orient": "split", - "dtype": { - "bus": "int64", - "element": "int64", - "et": "object", - "type": "object", - "closed": "bool", - "name": "object", - "z_ohm": "float64" - } - }, - "shunt": { - "_module": "pandas.core.frame", - "_class": "DataFrame", - "_object": "{\"columns\":[\"bus\",\"name\",\"q_mvar\",\"p_mw\",\"vn_kv\",\"step\",\"max_step\",\"in_service\"],\"index\":[0],\"data\":[[8,null,-19.0,0.0,20.0,1,1,true]]}", - "orient": "split", - "dtype": { - "bus": "uint32", - "name": "object", - "q_mvar": "float64", - "p_mw": "float64", - "vn_kv": "float64", - "step": "uint32", - "max_step": "uint32", - "in_service": "bool" - } - }, - "ext_grid": { - "_module": "pandas.core.frame", - "_class": "DataFrame", - "_object": "{\"columns\":[\"name\",\"bus\",\"vm_pu\",\"va_degree\",\"in_service\",\"min_p_mw\",\"max_p_mw\",\"min_q_mvar\",\"max_q_mvar\"],\"index\":[0],\"data\":[[null,0,1.06,0.0,true,0.0,332.399999999999977,0.0,10.0]]}", - "orient": "split", - "dtype": { - "name": "object", - "bus": "uint32", - "vm_pu": "float64", - "va_degree": "float64", - "in_service": "bool", - "min_p_mw": "float64", - "max_p_mw": "float64", - "min_q_mvar": "float64", - "max_q_mvar": "float64" - } - }, - "line": { - "_module": "pandas.core.frame", - "_class": "DataFrame", - "_object": "{\"columns\":[\"name\",\"std_type\",\"from_bus\",\"to_bus\",\"length_km\",\"r_ohm_per_km\",\"x_ohm_per_km\",\"c_nf_per_km\",\"g_us_per_km\",\"max_i_ka\",\"df\",\"parallel\",\"type\",\"in_service\",\"max_loading_percent\"],\"index\":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14],\"data\":[[null,null,0,1,1.0,3.6907272,11.2683348,882.522683811391971,0.0,41.418606267951418,1.0,1,\"ol\",true,100.0],[null,null,0,4,1.0,10.2894732,42.475737599999995,822.350682642433412,0.0,41.418606267951418,1.0,1,\"ol\",true,100.0],[null,null,1,2,1.0,8.948775599999999,37.701406800000001,732.092680888995574,0.0,41.418606267951418,1.0,1,\"ol\",true,100.0],[null,null,1,3,1.0,11.0664684,33.578380799999998,568.29112215127509,0.0,41.418606267951418,1.0,1,\"ol\",true,100.0],[null,null,1,4,1.0,10.845558,33.1137072,578.319789012768069,0.0,41.418606267951418,1.0,1,\"ol\",true,100.0],[null,null,2,3,1.0,12.761384400000001,32.570953199999998,213.94489304518595,0.0,41.418606267951418,1.0,1,\"ol\",true,100.0],[null,null,3,4,1.0,2.542374,8.019428400000001,0.0,0.0,41.418606267951418,1.0,1,\"ol\",true,100.0],[null,null,5,10,1.0,0.37992,0.7956,0.0,0.0,285.788383248864761,1.0,1,\"ol\",true,100.0],[null,null,5,11,1.0,0.49164,1.02324,0.0,0.0,285.788383248864761,1.0,1,\"ol\",true,100.0],[null,null,5,12,1.0,0.2646,0.52108,0.0,0.0,285.788383248864761,1.0,1,\"ol\",true,100.0],[null,null,8,9,1.0,0.12724,0.338,0.0,0.0,285.788383248864761,1.0,1,\"ol\",true,100.0],[null,null,8,13,1.0,0.50844,1.08152,0.0,0.0,285.788383248864761,1.0,1,\"ol\",true,100.0],[null,null,9,10,1.0,0.3282,0.76828,0.0,0.0,285.788383248864761,1.0,1,\"ol\",true,100.0],[null,null,11,12,1.0,0.88368,0.79952,0.0,0.0,285.788383248864761,1.0,1,\"ol\",true,100.0],[null,null,12,13,1.0,0.68372,1.39208,0.0,0.0,285.788383248864761,1.0,1,\"ol\",true,100.0]]}", - "orient": "split", - "dtype": { - "name": "object", - "std_type": "object", - "from_bus": "uint32", - "to_bus": "uint32", - "length_km": "float64", - "r_ohm_per_km": "float64", - "x_ohm_per_km": "float64", - "c_nf_per_km": "float64", - "g_us_per_km": "float64", - "max_i_ka": "float64", - "df": "float64", - "parallel": "uint32", - "type": "object", - "in_service": "bool", - "max_loading_percent": "float64" - } - }, - "trafo": { - "_module": "pandas.core.frame", - "_class": "DataFrame", - "_object": "{\"columns\":[\"name\",\"std_type\",\"hv_bus\",\"lv_bus\",\"sn_mva\",\"vn_hv_kv\",\"vn_lv_kv\",\"vk_percent\",\"vkr_percent\",\"pfe_kw\",\"i0_percent\",\"shift_degree\",\"tap_side\",\"tap_neutral\",\"tap_min\",\"tap_max\",\"tap_step_percent\",\"tap_step_degree\",\"tap_pos\",\"tap_phase_shifter\",\"parallel\",\"df\",\"in_service\",\"max_loading_percent\"],\"index\":[0,1,2,3,4],\"data\":[[null,null,3,6,9900.0,138.0,14.0,2070.288000000000011,0.0,0.0,0.0,0.0,\"hv\",0.0,null,null,2.200000000000002,null,-1.0,false,1,1.0,true,100.0],[null,null,3,8,9900.0,138.0,20.0,5506.181999999999789,0.0,0.0,0.0,0.0,\"hv\",0.0,null,null,3.100000000000003,null,-1.0,false,1,1.0,true,100.0],[null,null,4,5,9900.0,138.0,20.0,2494.998000000000047,0.0,0.0,0.0,0.0,\"hv\",0.0,null,null,6.799999999999995,null,-1.0,false,1,1.0,true,100.0],[null,null,6,7,9900.0,14.0,12.0,1743.884999999999991,0.0,0.0,0.0,0.0,null,null,null,null,null,null,null,false,1,1.0,true,100.0],[null,null,8,6,9900.0,20.0,14.0,1089.098999999999933,0.0,0.0,0.0,0.0,null,null,null,null,null,null,null,false,1,1.0,true,100.0]]}", - "orient": "split", - "dtype": { - "name": "object", - "std_type": "object", - "hv_bus": "uint32", - "lv_bus": "uint32", - "sn_mva": "float64", - "vn_hv_kv": "float64", - "vn_lv_kv": "float64", - "vk_percent": "float64", - "vkr_percent": "float64", - "pfe_kw": "float64", - "i0_percent": "float64", - "shift_degree": "float64", - "tap_side": "object", - "tap_neutral": "float64", - "tap_min": "float64", - "tap_max": "float64", - "tap_step_percent": "float64", - "tap_step_degree": "float64", - "tap_pos": "float64", - "tap_phase_shifter": "bool", - "parallel": "uint32", - "df": "float64", - "in_service": "bool", - "max_loading_percent": "float64" - } - }, - "trafo3w": { - "_module": "pandas.core.frame", - "_class": "DataFrame", - "_object": "{\"columns\":[\"name\",\"std_type\",\"hv_bus\",\"mv_bus\",\"lv_bus\",\"sn_hv_mva\",\"sn_mv_mva\",\"sn_lv_mva\",\"vn_hv_kv\",\"vn_mv_kv\",\"vn_lv_kv\",\"vk_hv_percent\",\"vk_mv_percent\",\"vk_lv_percent\",\"vkr_hv_percent\",\"vkr_mv_percent\",\"vkr_lv_percent\",\"pfe_kw\",\"i0_percent\",\"shift_mv_degree\",\"shift_lv_degree\",\"tap_side\",\"tap_neutral\",\"tap_min\",\"tap_max\",\"tap_step_percent\",\"tap_step_degree\",\"tap_pos\",\"tap_at_star_point\",\"in_service\"],\"index\":[],\"data\":[]}", - "orient": "split", - "dtype": { - "name": "object", - "std_type": "object", - "hv_bus": "uint32", - "mv_bus": "uint32", - "lv_bus": "uint32", - "sn_hv_mva": "float64", - "sn_mv_mva": "float64", - "sn_lv_mva": "float64", - "vn_hv_kv": "float64", - "vn_mv_kv": "float64", - "vn_lv_kv": "float64", - "vk_hv_percent": "float64", - "vk_mv_percent": "float64", - "vk_lv_percent": "float64", - "vkr_hv_percent": "float64", - "vkr_mv_percent": "float64", - "vkr_lv_percent": "float64", - "pfe_kw": "float64", - "i0_percent": "float64", - "shift_mv_degree": "float64", - "shift_lv_degree": "float64", - "tap_side": "object", - "tap_neutral": "int32", - "tap_min": "int32", - "tap_max": "int32", - "tap_step_percent": "float64", - "tap_step_degree": "float64", - "tap_pos": "int32", - "tap_at_star_point": "bool", - "in_service": "bool" - } - }, - "impedance": { - "_module": "pandas.core.frame", - "_class": "DataFrame", - "_object": "{\"columns\":[\"name\",\"from_bus\",\"to_bus\",\"rft_pu\",\"xft_pu\",\"rtf_pu\",\"xtf_pu\",\"sn_mva\",\"in_service\"],\"index\":[],\"data\":[]}", - "orient": "split", - "dtype": { - "name": "object", - "from_bus": "uint32", - "to_bus": "uint32", - "rft_pu": "float64", - "xft_pu": "float64", - "rtf_pu": "float64", - "xtf_pu": "float64", - "sn_mva": "float64", - "in_service": "bool" - } - }, - "dcline": { - "_module": "pandas.core.frame", - "_class": "DataFrame", - "_object": "{\"columns\":[\"name\",\"from_bus\",\"to_bus\",\"p_mw\",\"loss_percent\",\"loss_mw\",\"vm_from_pu\",\"vm_to_pu\",\"max_p_mw\",\"min_q_from_mvar\",\"min_q_to_mvar\",\"max_q_from_mvar\",\"max_q_to_mvar\",\"in_service\"],\"index\":[],\"data\":[]}", - "orient": "split", - "dtype": { - "name": "object", - "from_bus": "uint32", - "to_bus": "uint32", - "p_mw": "float64", - "loss_percent": "float64", - "loss_mw": "float64", - "vm_from_pu": "float64", - "vm_to_pu": "float64", - "max_p_mw": "float64", - "min_q_from_mvar": "float64", - "min_q_to_mvar": "float64", - "max_q_from_mvar": "float64", - "max_q_to_mvar": "float64", - "in_service": "bool" - } - }, - "ward": { - "_module": "pandas.core.frame", - "_class": "DataFrame", - "_object": "{\"columns\":[\"name\",\"bus\",\"ps_mw\",\"qs_mvar\",\"qz_mvar\",\"pz_mw\",\"in_service\"],\"index\":[],\"data\":[]}", - "orient": "split", - "dtype": { - "name": "object", - "bus": "uint32", - "ps_mw": "float64", - "qs_mvar": "float64", - "qz_mvar": "float64", - "pz_mw": "float64", - "in_service": "bool" - } - }, - "xward": { - "_module": "pandas.core.frame", - "_class": "DataFrame", - "_object": "{\"columns\":[\"name\",\"bus\",\"ps_mw\",\"qs_mvar\",\"qz_mvar\",\"pz_mw\",\"r_ohm\",\"x_ohm\",\"vm_pu\",\"in_service\"],\"index\":[],\"data\":[]}", - "orient": "split", - "dtype": { - "name": "object", - "bus": "uint32", - "ps_mw": "float64", - "qs_mvar": "float64", - "qz_mvar": "float64", - "pz_mw": "float64", - "r_ohm": "float64", - "x_ohm": "float64", - "vm_pu": "float64", - "in_service": "bool" - } - }, - "measurement": { - "_module": "pandas.core.frame", - "_class": "DataFrame", - "_object": "{\"columns\":[\"name\",\"measurement_type\",\"element_type\",\"element\",\"value\",\"std_dev\",\"side\"],\"index\":[],\"data\":[]}", - "orient": "split", - "dtype": { - "name": "object", - "measurement_type": "object", - "element_type": "object", - "element": "uint32", - "value": "float64", - "std_dev": "float64", - "side": "object" - } - }, - "pwl_cost": { - "_module": "pandas.core.frame", - "_class": "DataFrame", - "_object": "{\"columns\":[\"power_type\",\"element\",\"et\",\"points\"],\"index\":[],\"data\":[]}", - "orient": "split", - "dtype": { - "power_type": "object", - "element": "uint32", - "et": "object", - "points": "object" - } - }, - "poly_cost": { - "_module": "pandas.core.frame", - "_class": "DataFrame", - "_object": "{\"columns\":[\"element\",\"et\",\"cp0_eur\",\"cp1_eur_per_mw\",\"cp2_eur_per_mw2\",\"cq0_eur\",\"cq1_eur_per_mvar\",\"cq2_eur_per_mvar2\"],\"index\":[0,1,2,3,4],\"data\":[[0,\"ext_grid\",0.0,20.0,0.0430293,0.0,0.0,0.0],[0,\"gen\",0.0,20.0,0.25,0.0,0.0,0.0],[1,\"gen\",0.0,40.0,0.01,0.0,0.0,0.0],[2,\"gen\",0.0,40.0,0.01,0.0,0.0,0.0],[3,\"gen\",0.0,40.0,0.01,0.0,0.0,0.0]]}", - "orient": "split", - "dtype": { - "element": "uint32", - "et": "object", - "cp0_eur": "float64", - "cp1_eur_per_mw": "float64", - "cp2_eur_per_mw2": "float64", - "cq0_eur": "float64", - "cq1_eur_per_mvar": "float64", - "cq2_eur_per_mvar2": "float64" - } - }, - "controller": { - "_module": "pandas.core.frame", - "_class": "DataFrame", - "_object": "{\"columns\":[\"object\",\"in_service\",\"order\",\"level\",\"recycle\"],\"index\":[],\"data\":[]}", - "orient": "split", - "dtype": { - "object": "object", - "in_service": "bool", - "order": "float64", - "level": "object", - "recycle": "bool" - } - }, - "line_geodata": { - "_module": "pandas.core.frame", - "_class": "DataFrame", - "_object": "{\"columns\":[\"coords\"],\"index\":[],\"data\":[]}", - "orient": "split", - "dtype": { - "coords": "object" - } - }, - "bus_geodata": { - "_module": "pandas.core.frame", - "_class": "DataFrame", - "_object": "{\"columns\":[\"x\",\"y\",\"coords\"],\"index\":[],\"data\":[]}", - "orient": "split", - "dtype": { - "x": "float64", - "y": "float64", - "coords": "object" - } - }, - "version": "2.2.1", - "converged": false, - "name": "", - "f_hz": 50, - "sn_mva": 100.0, - "std_types": { - "line": { - "NAYY 4x50 SE": { - "c_nf_per_km": 210, - "r_ohm_per_km": 0.642, - "x_ohm_per_km": 0.083, - "max_i_ka": 0.142, - "type": "cs", - "q_mm2": 50, - "alpha": 0.00403 - }, - "NAYY 4x120 SE": { - "c_nf_per_km": 264, - "r_ohm_per_km": 0.225, - "x_ohm_per_km": 0.08, - "max_i_ka": 0.242, - "type": "cs", - "q_mm2": 120, - "alpha": 0.00403 - }, - "NAYY 4x150 SE": { - "c_nf_per_km": 261, - "r_ohm_per_km": 0.208, - "x_ohm_per_km": 0.08, - "max_i_ka": 0.27, - "type": "cs", - "q_mm2": 150, - "alpha": 0.00403 - }, - "NA2XS2Y 1x95 RM/25 12/20 kV": { - "c_nf_per_km": 216, - "r_ohm_per_km": 0.313, - "x_ohm_per_km": 0.132, - "max_i_ka": 0.252, - "type": "cs", - "q_mm2": 95, - "alpha": 0.00403 - }, - "NA2XS2Y 1x185 RM/25 12/20 kV": { - "c_nf_per_km": 273, - "r_ohm_per_km": 0.161, - "x_ohm_per_km": 0.117, - "max_i_ka": 0.362, - "type": "cs", - "q_mm2": 185, - "alpha": 0.00403 - }, - "NA2XS2Y 1x240 RM/25 12/20 kV": { - "c_nf_per_km": 304, - "r_ohm_per_km": 0.122, - "x_ohm_per_km": 0.112, - "max_i_ka": 0.421, - "type": "cs", - "q_mm2": 240, - "alpha": 0.00403 - }, - "NA2XS2Y 1x95 RM/25 6/10 kV": { - "c_nf_per_km": 315, - "r_ohm_per_km": 0.313, - "x_ohm_per_km": 0.123, - "max_i_ka": 0.249, - "type": "cs", - "q_mm2": 95, - "alpha": 0.00403 - }, - "NA2XS2Y 1x185 RM/25 6/10 kV": { - "c_nf_per_km": 406, - "r_ohm_per_km": 0.161, - "x_ohm_per_km": 0.11, - "max_i_ka": 0.358, - "type": "cs", - "q_mm2": 185, - "alpha": 0.00403 - }, - "NA2XS2Y 1x240 RM/25 6/10 kV": { - "c_nf_per_km": 456, - "r_ohm_per_km": 0.122, - "x_ohm_per_km": 0.105, - "max_i_ka": 0.416, - "type": "cs", - "q_mm2": 240, - "alpha": 0.00403 - }, - "NA2XS2Y 1x150 RM/25 12/20 kV": { - "c_nf_per_km": 250, - "r_ohm_per_km": 0.206, - "x_ohm_per_km": 0.116, - "max_i_ka": 0.319, - "type": "cs", - "q_mm2": 150, - "alpha": 0.00403 - }, - "NA2XS2Y 1x120 RM/25 12/20 kV": { - "c_nf_per_km": 230, - "r_ohm_per_km": 0.253, - "x_ohm_per_km": 0.119, - "max_i_ka": 0.283, - "type": "cs", - "q_mm2": 120, - "alpha": 0.00403 - }, - "NA2XS2Y 1x70 RM/25 12/20 kV": { - "c_nf_per_km": 190, - "r_ohm_per_km": 0.443, - "x_ohm_per_km": 0.132, - "max_i_ka": 0.22, - "type": "cs", - "q_mm2": 70, - "alpha": 0.00403 - }, - "NA2XS2Y 1x150 RM/25 6/10 kV": { - "c_nf_per_km": 360, - "r_ohm_per_km": 0.206, - "x_ohm_per_km": 0.11, - "max_i_ka": 0.315, - "type": "cs", - "q_mm2": 150, - "alpha": 0.00403 - }, - "NA2XS2Y 1x120 RM/25 6/10 kV": { - "c_nf_per_km": 340, - "r_ohm_per_km": 0.253, - "x_ohm_per_km": 0.113, - "max_i_ka": 0.28, - "type": "cs", - "q_mm2": 120, - "alpha": 0.00403 - }, - "NA2XS2Y 1x70 RM/25 6/10 kV": { - "c_nf_per_km": 280, - "r_ohm_per_km": 0.443, - "x_ohm_per_km": 0.123, - "max_i_ka": 0.217, - "type": "cs", - "q_mm2": 70, - "alpha": 0.00403 - }, - "N2XS(FL)2Y 1x120 RM/35 64/110 kV": { - "c_nf_per_km": 112, - "r_ohm_per_km": 0.153, - "x_ohm_per_km": 0.166, - "max_i_ka": 0.366, - "type": "cs", - "q_mm2": 120, - "alpha": 0.00393 - }, - "N2XS(FL)2Y 1x185 RM/35 64/110 kV": { - "c_nf_per_km": 125, - "r_ohm_per_km": 0.099, - "x_ohm_per_km": 0.156, - "max_i_ka": 0.457, - "type": "cs", - "q_mm2": 185, - "alpha": 0.00393 - }, - "N2XS(FL)2Y 1x240 RM/35 64/110 kV": { - "c_nf_per_km": 135, - "r_ohm_per_km": 0.075, - "x_ohm_per_km": 0.149, - "max_i_ka": 0.526, - "type": "cs", - "q_mm2": 240, - "alpha": 0.00393 - }, - "N2XS(FL)2Y 1x300 RM/35 64/110 kV": { - "c_nf_per_km": 144, - "r_ohm_per_km": 0.06, - "x_ohm_per_km": 0.144, - "max_i_ka": 0.588, - "type": "cs", - "q_mm2": 300, - "alpha": 0.00393 - }, - "15-AL1/3-ST1A 0.4": { - "c_nf_per_km": 11, - "r_ohm_per_km": 1.8769, - "x_ohm_per_km": 0.35, - "max_i_ka": 0.105, - "type": "ol", - "q_mm2": 16, - "alpha": 0.00403 - }, - "24-AL1/4-ST1A 0.4": { - "c_nf_per_km": 11.25, - "r_ohm_per_km": 1.2012, - "x_ohm_per_km": 0.335, - "max_i_ka": 0.14, - "type": "ol", - "q_mm2": 24, - "alpha": 0.00403 - }, - "48-AL1/8-ST1A 0.4": { - "c_nf_per_km": 12.2, - "r_ohm_per_km": 0.5939, - "x_ohm_per_km": 0.3, - "max_i_ka": 0.21, - "type": "ol", - "q_mm2": 48, - "alpha": 0.00403 - }, - "94-AL1/15-ST1A 0.4": { - "c_nf_per_km": 13.2, - "r_ohm_per_km": 0.306, - "x_ohm_per_km": 0.29, - "max_i_ka": 0.35, - "type": "ol", - "q_mm2": 94, - "alpha": 0.00403 - }, - "34-AL1/6-ST1A 10.0": { - "c_nf_per_km": 9.7, - "r_ohm_per_km": 0.8342, - "x_ohm_per_km": 0.36, - "max_i_ka": 0.17, - "type": "ol", - "q_mm2": 34, - "alpha": 0.00403 - }, - "48-AL1/8-ST1A 10.0": { - "c_nf_per_km": 10.1, - "r_ohm_per_km": 0.5939, - "x_ohm_per_km": 0.35, - "max_i_ka": 0.21, - "type": "ol", - "q_mm2": 48, - "alpha": 0.00403 - }, - "70-AL1/11-ST1A 10.0": { - "c_nf_per_km": 10.4, - "r_ohm_per_km": 0.4132, - "x_ohm_per_km": 0.339, - "max_i_ka": 0.29, - "type": "ol", - "q_mm2": 70, - "alpha": 0.00403 - }, - "94-AL1/15-ST1A 10.0": { - "c_nf_per_km": 10.75, - "r_ohm_per_km": 0.306, - "x_ohm_per_km": 0.33, - "max_i_ka": 0.35, - "type": "ol", - "q_mm2": 94, - "alpha": 0.00403 - }, - "122-AL1/20-ST1A 10.0": { - "c_nf_per_km": 11.1, - "r_ohm_per_km": 0.2376, - "x_ohm_per_km": 0.323, - "max_i_ka": 0.41, - "type": "ol", - "q_mm2": 122, - "alpha": 0.00403 - }, - "149-AL1/24-ST1A 10.0": { - "c_nf_per_km": 11.25, - "r_ohm_per_km": 0.194, - "x_ohm_per_km": 0.315, - "max_i_ka": 0.47, - "type": "ol", - "q_mm2": 149, - "alpha": 0.00403 - }, - "34-AL1/6-ST1A 20.0": { - "c_nf_per_km": 9.15, - "r_ohm_per_km": 0.8342, - "x_ohm_per_km": 0.382, - "max_i_ka": 0.17, - "type": "ol", - "q_mm2": 34, - "alpha": 0.00403 - }, - "48-AL1/8-ST1A 20.0": { - "c_nf_per_km": 9.5, - "r_ohm_per_km": 0.5939, - "x_ohm_per_km": 0.372, - "max_i_ka": 0.21, - "type": "ol", - "q_mm2": 48, - "alpha": 0.00403 - }, - "70-AL1/11-ST1A 20.0": { - "c_nf_per_km": 9.7, - "r_ohm_per_km": 0.4132, - "x_ohm_per_km": 0.36, - "max_i_ka": 0.29, - "type": "ol", - "q_mm2": 70, - "alpha": 0.00403 - }, - "94-AL1/15-ST1A 20.0": { - "c_nf_per_km": 10, - "r_ohm_per_km": 0.306, - "x_ohm_per_km": 0.35, - "max_i_ka": 0.35, - "type": "ol", - "q_mm2": 94, - "alpha": 0.00403 - }, - "122-AL1/20-ST1A 20.0": { - "c_nf_per_km": 10.3, - "r_ohm_per_km": 0.2376, - "x_ohm_per_km": 0.344, - "max_i_ka": 0.41, - "type": "ol", - "q_mm2": 122, - "alpha": 0.00403 - }, - "149-AL1/24-ST1A 20.0": { - "c_nf_per_km": 10.5, - "r_ohm_per_km": 0.194, - "x_ohm_per_km": 0.337, - "max_i_ka": 0.47, - "type": "ol", - "q_mm2": 149, - "alpha": 0.00403 - }, - "184-AL1/30-ST1A 20.0": { - "c_nf_per_km": 10.75, - "r_ohm_per_km": 0.1571, - "x_ohm_per_km": 0.33, - "max_i_ka": 0.535, - "type": "ol", - "q_mm2": 184, - "alpha": 0.00403 - }, - "243-AL1/39-ST1A 20.0": { - "c_nf_per_km": 11, - "r_ohm_per_km": 0.1188, - "x_ohm_per_km": 0.32, - "max_i_ka": 0.645, - "type": "ol", - "q_mm2": 243, - "alpha": 0.00403 - }, - "48-AL1/8-ST1A 110.0": { - "c_nf_per_km": 8, - "r_ohm_per_km": 0.5939, - "x_ohm_per_km": 0.46, - "max_i_ka": 0.21, - "type": "ol", - "q_mm2": 48, - "alpha": 0.00403 - }, - "70-AL1/11-ST1A 110.0": { - "c_nf_per_km": 8.4, - "r_ohm_per_km": 0.4132, - "x_ohm_per_km": 0.45, - "max_i_ka": 0.29, - "type": "ol", - "q_mm2": 70, - "alpha": 0.00403 - }, - "94-AL1/15-ST1A 110.0": { - "c_nf_per_km": 8.65, - "r_ohm_per_km": 0.306, - "x_ohm_per_km": 0.44, - "max_i_ka": 0.35, - "type": "ol", - "q_mm2": 94, - "alpha": 0.00403 - }, - "122-AL1/20-ST1A 110.0": { - "c_nf_per_km": 8.5, - "r_ohm_per_km": 0.2376, - "x_ohm_per_km": 0.43, - "max_i_ka": 0.41, - "type": "ol", - "q_mm2": 122, - "alpha": 0.00403 - }, - "149-AL1/24-ST1A 110.0": { - "c_nf_per_km": 8.75, - "r_ohm_per_km": 0.194, - "x_ohm_per_km": 0.41, - "max_i_ka": 0.47, - "type": "ol", - "q_mm2": 149, - "alpha": 0.00403 - }, - "184-AL1/30-ST1A 110.0": { - "c_nf_per_km": 8.8, - "r_ohm_per_km": 0.1571, - "x_ohm_per_km": 0.4, - "max_i_ka": 0.535, - "type": "ol", - "q_mm2": 184, - "alpha": 0.00403 - }, - "243-AL1/39-ST1A 110.0": { - "c_nf_per_km": 9, - "r_ohm_per_km": 0.1188, - "x_ohm_per_km": 0.39, - "max_i_ka": 0.645, - "type": "ol", - "q_mm2": 243, - "alpha": 0.00403 - }, - "305-AL1/39-ST1A 110.0": { - "c_nf_per_km": 9.2, - "r_ohm_per_km": 0.0949, - "x_ohm_per_km": 0.38, - "max_i_ka": 0.74, - "type": "ol", - "q_mm2": 305, - "alpha": 0.00403 - }, - "490-AL1/64-ST1A 110.0": { - "c_nf_per_km": 9.75, - "r_ohm_per_km": 0.059, - "x_ohm_per_km": 0.37, - "max_i_ka": 0.96, - "type": "ol", - "q_mm2": 490, - "alpha": 0.00403 - }, - "679-AL1/86-ST1A 110.0": { - "c_nf_per_km": 9.95, - "r_ohm_per_km": 0.042, - "x_ohm_per_km": 0.36, - "max_i_ka": 1.15, - "type": "ol", - "q_mm2": 679, - "alpha": 0.00403 - }, - "490-AL1/64-ST1A 220.0": { - "c_nf_per_km": 10, - "r_ohm_per_km": 0.059, - "x_ohm_per_km": 0.285, - "max_i_ka": 0.96, - "type": "ol", - "q_mm2": 490, - "alpha": 0.00403 - }, - "679-AL1/86-ST1A 220.0": { - "c_nf_per_km": 11.7, - "r_ohm_per_km": 0.042, - "x_ohm_per_km": 0.275, - "max_i_ka": 1.15, - "type": "ol", - "q_mm2": 679, - "alpha": 0.00403 - }, - "490-AL1/64-ST1A 380.0": { - "c_nf_per_km": 11, - "r_ohm_per_km": 0.059, - "x_ohm_per_km": 0.253, - "max_i_ka": 0.96, - "type": "ol", - "q_mm2": 490, - "alpha": 0.00403 - }, - "679-AL1/86-ST1A 380.0": { - "c_nf_per_km": 14.6, - "r_ohm_per_km": 0.042, - "x_ohm_per_km": 0.25, - "max_i_ka": 1.15, - "type": "ol", - "q_mm2": 679, - "alpha": 0.00403 - } - }, - "trafo": { - "160 MVA 380/110 kV": { - "i0_percent": 0.06, - "pfe_kw": 60, - "vkr_percent": 0.25, - "sn_mva": 160, - "vn_lv_kv": 110.0, - "vn_hv_kv": 380.0, - "vk_percent": 12.2, - "shift_degree": 0, - "vector_group": "Yy0", - "tap_side": "hv", - "tap_neutral": 0, - "tap_min": -9, - "tap_max": 9, - "tap_step_degree": 0, - "tap_step_percent": 1.5, - "tap_phase_shifter": false - }, - "100 MVA 220/110 kV": { - "i0_percent": 0.06, - "pfe_kw": 55, - "vkr_percent": 0.26, - "sn_mva": 100, - "vn_lv_kv": 110.0, - "vn_hv_kv": 220.0, - "vk_percent": 12.0, - "shift_degree": 0, - "vector_group": "Yy0", - "tap_side": "hv", - "tap_neutral": 0, - "tap_min": -9, - "tap_max": 9, - "tap_step_degree": 0, - "tap_step_percent": 1.5, - "tap_phase_shifter": false - }, - "63 MVA 110/20 kV": { - "i0_percent": 0.04, - "pfe_kw": 22, - "vkr_percent": 0.32, - "sn_mva": 63, - "vn_lv_kv": 20.0, - "vn_hv_kv": 110.0, - "vk_percent": 18, - "shift_degree": 150, - "vector_group": "YNd5", - "tap_side": "hv", - "tap_neutral": 0, - "tap_min": -9, - "tap_max": 9, - "tap_step_degree": 0, - "tap_step_percent": 1.5, - "tap_phase_shifter": false - }, - "40 MVA 110/20 kV": { - "i0_percent": 0.05, - "pfe_kw": 18, - "vkr_percent": 0.34, - "sn_mva": 40, - "vn_lv_kv": 20.0, - "vn_hv_kv": 110.0, - "vk_percent": 16.2, - "shift_degree": 150, - "vector_group": "YNd5", - "tap_side": "hv", - "tap_neutral": 0, - "tap_min": -9, - "tap_max": 9, - "tap_step_degree": 0, - "tap_step_percent": 1.5, - "tap_phase_shifter": false - }, - "25 MVA 110/20 kV": { - "i0_percent": 0.07, - "pfe_kw": 14, - "vkr_percent": 0.41, - "sn_mva": 25, - "vn_lv_kv": 20.0, - "vn_hv_kv": 110.0, - "vk_percent": 12, - "shift_degree": 150, - "vector_group": "YNd5", - "tap_side": "hv", - "tap_neutral": 0, - "tap_min": -9, - "tap_max": 9, - "tap_step_degree": 0, - "tap_step_percent": 1.5, - "tap_phase_shifter": false - }, - "63 MVA 110/10 kV": { - "sn_mva": 63, - "vn_hv_kv": 110, - "vn_lv_kv": 10, - "vk_percent": 18, - "vkr_percent": 0.32, - "pfe_kw": 22, - "i0_percent": 0.04, - "shift_degree": 150, - "vector_group": "YNd5", - "tap_side": "hv", - "tap_neutral": 0, - "tap_min": -9, - "tap_max": 9, - "tap_step_degree": 0, - "tap_step_percent": 1.5, - "tap_phase_shifter": false - }, - "40 MVA 110/10 kV": { - "sn_mva": 40, - "vn_hv_kv": 110, - "vn_lv_kv": 10, - "vk_percent": 16.2, - "vkr_percent": 0.34, - "pfe_kw": 18, - "i0_percent": 0.05, - "shift_degree": 150, - "vector_group": "YNd5", - "tap_side": "hv", - "tap_neutral": 0, - "tap_min": -9, - "tap_max": 9, - "tap_step_degree": 0, - "tap_step_percent": 1.5, - "tap_phase_shifter": false - }, - "25 MVA 110/10 kV": { - "sn_mva": 25, - "vn_hv_kv": 110, - "vn_lv_kv": 10, - "vk_percent": 12, - "vkr_percent": 0.41, - "pfe_kw": 14, - "i0_percent": 0.07, - "shift_degree": 150, - "vector_group": "YNd5", - "tap_side": "hv", - "tap_neutral": 0, - "tap_min": -9, - "tap_max": 9, - "tap_step_degree": 0, - "tap_step_percent": 1.5, - "tap_phase_shifter": false - }, - "0.25 MVA 20/0.4 kV": { - "sn_mva": 0.25, - "vn_hv_kv": 20, - "vn_lv_kv": 0.4, - "vk_percent": 6, - "vkr_percent": 1.44, - "pfe_kw": 0.8, - "i0_percent": 0.32, - "shift_degree": 150, - "vector_group": "Yzn5", - "tap_side": "hv", - "tap_neutral": 0, - "tap_min": -2, - "tap_max": 2, - "tap_step_degree": 0, - "tap_step_percent": 2.5, - "tap_phase_shifter": false - }, - "0.4 MVA 20/0.4 kV": { - "sn_mva": 0.4, - "vn_hv_kv": 20, - "vn_lv_kv": 0.4, - "vk_percent": 6, - "vkr_percent": 1.425, - "pfe_kw": 1.35, - "i0_percent": 0.3375, - "shift_degree": 150, - "vector_group": "Dyn5", - "tap_side": "hv", - "tap_neutral": 0, - "tap_min": -2, - "tap_max": 2, - "tap_step_degree": 0, - "tap_step_percent": 2.5, - "tap_phase_shifter": false - }, - "0.63 MVA 20/0.4 kV": { - "sn_mva": 0.63, - "vn_hv_kv": 20, - "vn_lv_kv": 0.4, - "vk_percent": 6, - "vkr_percent": 1.206, - "pfe_kw": 1.65, - "i0_percent": 0.2619, - "shift_degree": 150, - "vector_group": "Dyn5", - "tap_side": "hv", - "tap_neutral": 0, - "tap_min": -2, - "tap_max": 2, - "tap_step_degree": 0, - "tap_step_percent": 2.5, - "tap_phase_shifter": false - }, - "0.25 MVA 10/0.4 kV": { - "sn_mva": 0.25, - "vn_hv_kv": 10, - "vn_lv_kv": 0.4, - "vk_percent": 4, - "vkr_percent": 1.2, - "pfe_kw": 0.6, - "i0_percent": 0.24, - "shift_degree": 150, - "vector_group": "Dyn5", - "tap_side": "hv", - "tap_neutral": 0, - "tap_min": -2, - "tap_max": 2, - "tap_step_degree": 0, - "tap_step_percent": 2.5, - "tap_phase_shifter": false - }, - "0.4 MVA 10/0.4 kV": { - "sn_mva": 0.4, - "vn_hv_kv": 10, - "vn_lv_kv": 0.4, - "vk_percent": 4, - "vkr_percent": 1.325, - "pfe_kw": 0.95, - "i0_percent": 0.2375, - "shift_degree": 150, - "vector_group": "Dyn5", - "tap_side": "hv", - "tap_neutral": 0, - "tap_min": -2, - "tap_max": 2, - "tap_step_degree": 0, - "tap_step_percent": 2.5, - "tap_phase_shifter": false - }, - "0.63 MVA 10/0.4 kV": { - "sn_mva": 0.63, - "vn_hv_kv": 10, - "vn_lv_kv": 0.4, - "vk_percent": 4, - "vkr_percent": 1.0794, - "pfe_kw": 1.18, - "i0_percent": 0.1873, - "shift_degree": 150, - "vector_group": "Dyn5", - "tap_side": "hv", - "tap_neutral": 0, - "tap_min": -2, - "tap_max": 2, - "tap_step_degree": 0, - "tap_step_percent": 2.5, - "tap_phase_shifter": false - } - }, - "trafo3w": { - "63/25/38 MVA 110/20/10 kV": { - "sn_hv_mva": 63, - "sn_mv_mva": 25, - "sn_lv_mva": 38, - "vn_hv_kv": 110, - "vn_mv_kv": 20, - "vn_lv_kv": 10, - "vk_hv_percent": 10.4, - "vk_mv_percent": 10.4, - "vk_lv_percent": 10.4, - "vkr_hv_percent": 0.28, - "vkr_mv_percent": 0.32, - "vkr_lv_percent": 0.35, - "pfe_kw": 35, - "i0_percent": 0.89, - "shift_mv_degree": 0, - "shift_lv_degree": 0, - "vector_group": "YN0yn0yn0", - "tap_side": "hv", - "tap_neutral": 0, - "tap_min": -10, - "tap_max": 10, - "tap_step_percent": 1.2 - }, - "63/25/38 MVA 110/10/10 kV": { - "sn_hv_mva": 63, - "sn_mv_mva": 25, - "sn_lv_mva": 38, - "vn_hv_kv": 110, - "vn_mv_kv": 10, - "vn_lv_kv": 10, - "vk_hv_percent": 10.4, - "vk_mv_percent": 10.4, - "vk_lv_percent": 10.4, - "vkr_hv_percent": 0.28, - "vkr_mv_percent": 0.32, - "vkr_lv_percent": 0.35, - "pfe_kw": 35, - "i0_percent": 0.89, - "shift_mv_degree": 0, - "shift_lv_degree": 0, - "vector_group": "YN0yn0yn0", - "tap_side": "hv", - "tap_neutral": 0, - "tap_min": -10, - "tap_max": 10, - "tap_step_percent": 1.2 - } - } - }, - "res_bus": { - "_module": "pandas.core.frame", - "_class": "DataFrame", - "_object": "{\"columns\":[\"vm_pu\",\"va_degree\",\"p_mw\",\"q_mvar\"],\"index\":[],\"data\":[]}", - "orient": "split", - "dtype": { - "vm_pu": "float64", - "va_degree": "float64", - "p_mw": "float64", - "q_mvar": "float64" - } - }, - "res_line": { - "_module": "pandas.core.frame", - "_class": "DataFrame", - "_object": "{\"columns\":[\"p_from_mw\",\"q_from_mvar\",\"p_to_mw\",\"q_to_mvar\",\"pl_mw\",\"ql_mvar\",\"i_from_ka\",\"i_to_ka\",\"i_ka\",\"vm_from_pu\",\"va_from_degree\",\"vm_to_pu\",\"va_to_degree\",\"loading_percent\"],\"index\":[],\"data\":[]}", - "orient": "split", - "dtype": { - "p_from_mw": "float64", - "q_from_mvar": "float64", - "p_to_mw": "float64", - "q_to_mvar": "float64", - "pl_mw": "float64", - "ql_mvar": "float64", - "i_from_ka": "float64", - "i_to_ka": "float64", - "i_ka": "float64", - "vm_from_pu": "float64", - "va_from_degree": "float64", - "vm_to_pu": "float64", - "va_to_degree": "float64", - "loading_percent": "float64" - } - }, - "res_trafo": { - "_module": "pandas.core.frame", - "_class": "DataFrame", - "_object": "{\"columns\":[\"p_hv_mw\",\"q_hv_mvar\",\"p_lv_mw\",\"q_lv_mvar\",\"pl_mw\",\"ql_mvar\",\"i_hv_ka\",\"i_lv_ka\",\"vm_hv_pu\",\"va_hv_degree\",\"vm_lv_pu\",\"va_lv_degree\",\"loading_percent\"],\"index\":[],\"data\":[]}", - "orient": "split", - "dtype": { - "p_hv_mw": "float64", - "q_hv_mvar": "float64", - "p_lv_mw": "float64", - "q_lv_mvar": "float64", - "pl_mw": "float64", - "ql_mvar": "float64", - "i_hv_ka": "float64", - "i_lv_ka": "float64", - "vm_hv_pu": "float64", - "va_hv_degree": "float64", - "vm_lv_pu": "float64", - "va_lv_degree": "float64", - "loading_percent": "float64" - } - }, - "res_trafo3w": { - "_module": "pandas.core.frame", - "_class": "DataFrame", - "_object": "{\"columns\":[\"p_hv_mw\",\"q_hv_mvar\",\"p_mv_mw\",\"q_mv_mvar\",\"p_lv_mw\",\"q_lv_mvar\",\"pl_mw\",\"ql_mvar\",\"i_hv_ka\",\"i_mv_ka\",\"i_lv_ka\",\"vm_hv_pu\",\"va_hv_degree\",\"vm_mv_pu\",\"va_mv_degree\",\"vm_lv_pu\",\"va_lv_degree\",\"va_internal_degree\",\"vm_internal_pu\",\"loading_percent\"],\"index\":[],\"data\":[]}", - "orient": "split", - "dtype": { - "p_hv_mw": "float64", - "q_hv_mvar": "float64", - "p_mv_mw": "float64", - "q_mv_mvar": "float64", - "p_lv_mw": "float64", - "q_lv_mvar": "float64", - "pl_mw": "float64", - "ql_mvar": "float64", - "i_hv_ka": "float64", - "i_mv_ka": "float64", - "i_lv_ka": "float64", - "vm_hv_pu": "float64", - "va_hv_degree": "float64", - "vm_mv_pu": "float64", - "va_mv_degree": "float64", - "vm_lv_pu": "float64", - "va_lv_degree": "float64", - "va_internal_degree": "float64", - "vm_internal_pu": "float64", - "loading_percent": "float64" - } - }, - "res_impedance": { - "_module": "pandas.core.frame", - "_class": "DataFrame", - "_object": "{\"columns\":[\"p_from_mw\",\"q_from_mvar\",\"p_to_mw\",\"q_to_mvar\",\"pl_mw\",\"ql_mvar\",\"i_from_ka\",\"i_to_ka\"],\"index\":[],\"data\":[]}", - "orient": "split", - "dtype": { - "p_from_mw": "float64", - "q_from_mvar": "float64", - "p_to_mw": "float64", - "q_to_mvar": "float64", - "pl_mw": "float64", - "ql_mvar": "float64", - "i_from_ka": "float64", - "i_to_ka": "float64" - } - }, - "res_ext_grid": { - "_module": "pandas.core.frame", - "_class": "DataFrame", - "_object": "{\"columns\":[\"p_mw\",\"q_mvar\"],\"index\":[],\"data\":[]}", - "orient": "split", - "dtype": { - "p_mw": "float64", - "q_mvar": "float64" - } - }, - "res_load": { - "_module": "pandas.core.frame", - "_class": "DataFrame", - "_object": "{\"columns\":[\"p_mw\",\"q_mvar\"],\"index\":[],\"data\":[]}", - "orient": "split", - "dtype": { - "p_mw": "float64", - "q_mvar": "float64" - } - }, - "res_sgen": { - "_module": "pandas.core.frame", - "_class": "DataFrame", - "_object": "{\"columns\":[\"p_mw\",\"q_mvar\"],\"index\":[],\"data\":[]}", - "orient": "split", - "dtype": { - "p_mw": "float64", - "q_mvar": "float64" - } - }, - "res_storage": { - "_module": "pandas.core.frame", - "_class": "DataFrame", - "_object": "{\"columns\":[\"p_mw\",\"q_mvar\"],\"index\":[],\"data\":[]}", - "orient": "split", - "dtype": { - "p_mw": "float64", - "q_mvar": "float64" - } - }, - "res_shunt": { - "_module": "pandas.core.frame", - "_class": "DataFrame", - "_object": "{\"columns\":[\"p_mw\",\"q_mvar\",\"vm_pu\"],\"index\":[],\"data\":[]}", - "orient": "split", - "dtype": { - "p_mw": "float64", - "q_mvar": "float64", - "vm_pu": "float64" - } - }, - "res_gen": { - "_module": "pandas.core.frame", - "_class": "DataFrame", - "_object": "{\"columns\":[\"p_mw\",\"q_mvar\",\"va_degree\",\"vm_pu\"],\"index\":[],\"data\":[]}", - "orient": "split", - "dtype": { - "p_mw": "float64", - "q_mvar": "float64", - "va_degree": "float64", - "vm_pu": "float64" - } - }, - "res_ward": { - "_module": "pandas.core.frame", - "_class": "DataFrame", - "_object": "{\"columns\":[\"p_mw\",\"q_mvar\",\"vm_pu\"],\"index\":[],\"data\":[]}", - "orient": "split", - "dtype": { - "p_mw": "float64", - "q_mvar": "float64", - "vm_pu": "float64" - } - }, - "res_xward": { - "_module": "pandas.core.frame", - "_class": "DataFrame", - "_object": "{\"columns\":[\"p_mw\",\"q_mvar\",\"vm_pu\",\"va_internal_degree\",\"vm_internal_pu\"],\"index\":[],\"data\":[]}", - "orient": "split", - "dtype": { - "p_mw": "float64", - "q_mvar": "float64", - "vm_pu": "float64", - "va_internal_degree": "float64", - "vm_internal_pu": "float64" - } - }, - "res_dcline": { - "_module": "pandas.core.frame", - "_class": "DataFrame", - "_object": "{\"columns\":[\"p_from_mw\",\"q_from_mvar\",\"p_to_mw\",\"q_to_mvar\",\"pl_mw\",\"vm_from_pu\",\"va_from_degree\",\"vm_to_pu\",\"va_to_degree\"],\"index\":[],\"data\":[]}", - "orient": "split", - "dtype": { - "p_from_mw": "float64", - "q_from_mvar": "float64", - "p_to_mw": "float64", - "q_to_mvar": "float64", - "pl_mw": "float64", - "vm_from_pu": "float64", - "va_from_degree": "float64", - "vm_to_pu": "float64", - "va_to_degree": "float64" - } - }, - "user_pf_options": {} - } -} \ No newline at end of file + "_module": "pandapower.auxiliary", + "_class": "pandapowerNet", + "_object": "{\"OPF_converged\":false,\"bus\":{\n \"_module\": \"pandas.core.frame\",\n \"_class\": \"DataFrame\",\n \"_object\": \"{\\\"columns\\\":[\\\"in_service\\\",\\\"max_vm_pu\\\",\\\"min_vm_pu\\\",\\\"name\\\",\\\"type\\\",\\\"vn_kv\\\",\\\"zone\\\"],\\\"index\\\":[0,1,10,11,12,13,2,3,4,5,6,7,8,9],\\\"data\\\":[[true,1.06,0.94,1,\\\"b\\\",135.0,1.0],[true,1.06,0.94,2,\\\"b\\\",135.0,1.0],[true,1.06,0.94,11,\\\"b\\\",0.208,1.0],[true,1.06,0.94,12,\\\"b\\\",0.208,1.0],[true,1.06,0.94,13,\\\"b\\\",0.208,1.0],[true,1.06,0.94,14,\\\"b\\\",0.208,1.0],[true,1.06,0.94,3,\\\"b\\\",135.0,1.0],[true,1.06,0.94,4,\\\"b\\\",135.0,1.0],[true,1.06,0.94,5,\\\"b\\\",135.0,1.0],[true,1.06,0.94,6,\\\"b\\\",0.208,1.0],[true,1.06,0.94,7,\\\"b\\\",14.0,1.0],[true,1.06,0.94,8,\\\"b\\\",12.0,1.0],[true,1.06,0.94,9,\\\"b\\\",0.208,1.0],[true,1.06,0.94,10,\\\"b\\\",0.208,1.0]]}\",\n \"dtype\": {\n \"in_service\": \"bool\",\n \"max_vm_pu\": \"float64\",\n \"min_vm_pu\": \"float64\",\n \"name\": \"object\",\n \"type\": \"object\",\n \"vn_kv\": \"float64\",\n \"zone\": \"object\"\n },\n \"orient\": \"split\"\n},\"bus_geodata\":{\n \"_module\": \"pandas.core.frame\",\n \"_class\": \"DataFrame\",\n \"_object\": \"{\\\"columns\\\":[\\\"x\\\",\\\"y\\\",\\\"coords\\\"],\\\"index\\\":[0,1,10,11,12,13,2,3,4,5,6,7,8,9],\\\"data\\\":[[1.9673949894,-0.9610198739,null],[2.9779852289,-1.0412882366,null],[1.8366837619,1.0890065149,null],[2.3371166416,2.3091630377,null],[3.3094922817,2.1179802998,null],[4.3962052866,1.6847581464,null],[3.780660539,-1.6066859687,null],[3.8337344898,-0.4914657254,null],[2.6937067209,-0.095882852,null],[2.5321180205,1.2056156419,null],[4.8721406581,-0.2692952825,null],[5.9042747731,-0.5402149495,null],[4.274948799,0.5335379916,null],[3.2723067024,0.9619849305,null]]}\",\n \"dtype\": {\n \"x\": \"float64\",\n \"y\": \"float64\",\n \"coords\": \"object\"\n },\n \"orient\": \"split\"\n},\"converged\":true,\"dcline\":{\n \"_module\": \"pandas.core.frame\",\n \"_class\": \"DataFrame\",\n \"_object\": \"{\\\"columns\\\":[\\\"name\\\",\\\"from_bus\\\",\\\"to_bus\\\",\\\"p_mw\\\",\\\"loss_percent\\\",\\\"loss_mw\\\",\\\"vm_from_pu\\\",\\\"vm_to_pu\\\",\\\"max_p_mw\\\",\\\"min_q_from_mvar\\\",\\\"min_q_to_mvar\\\",\\\"max_q_from_mvar\\\",\\\"max_q_to_mvar\\\",\\\"in_service\\\"],\\\"index\\\":[],\\\"data\\\":[]}\",\n \"dtype\": {\n \"name\": \"object\",\n \"from_bus\": \"uint32\",\n \"to_bus\": \"uint32\",\n \"p_mw\": \"float64\",\n \"loss_percent\": \"float64\",\n \"loss_mw\": \"float64\",\n \"vm_from_pu\": \"float64\",\n \"vm_to_pu\": \"float64\",\n \"max_p_mw\": \"float64\",\n \"min_q_from_mvar\": \"float64\",\n \"min_q_to_mvar\": \"float64\",\n \"max_q_from_mvar\": \"float64\",\n \"max_q_to_mvar\": \"float64\",\n \"in_service\": \"bool\"\n },\n \"orient\": \"split\"\n},\"ext_grid\":{\n \"_module\": \"pandas.core.frame\",\n \"_class\": \"DataFrame\",\n \"_object\": \"{\\\"columns\\\":[\\\"bus\\\",\\\"in_service\\\",\\\"name\\\",\\\"va_degree\\\",\\\"vm_pu\\\",\\\"max_p_mw\\\",\\\"min_p_mw\\\",\\\"max_q_mvar\\\",\\\"min_q_mvar\\\"],\\\"index\\\":[0],\\\"data\\\":[[0,true,null,0.0,1.06,332.400000000000034,0.0,10.0,0.0]]}\",\n \"dtype\": {\n \"bus\": \"uint32\",\n \"in_service\": \"bool\",\n \"name\": \"object\",\n \"va_degree\": \"float64\",\n \"vm_pu\": \"float64\",\n \"max_p_mw\": \"float64\",\n \"min_p_mw\": \"float64\",\n \"max_q_mvar\": \"float64\",\n \"min_q_mvar\": \"float64\"\n },\n \"orient\": \"split\"\n},\"f_hz\":60,\"gen\":{\n \"_module\": \"pandas.core.frame\",\n \"_class\": \"DataFrame\",\n \"_object\": \"{\\\"columns\\\":[\\\"bus\\\",\\\"controllable\\\",\\\"in_service\\\",\\\"name\\\",\\\"p_mw\\\",\\\"scaling\\\",\\\"sn_mva\\\",\\\"type\\\",\\\"vm_pu\\\",\\\"slack\\\",\\\"max_p_mw\\\",\\\"min_p_mw\\\",\\\"max_q_mvar\\\",\\\"min_q_mvar\\\"],\\\"index\\\":[0,1,2,3],\\\"data\\\":[[1,true,true,null,40.0,1.0,null,null,1.045,false,140.0,0.0,50.0,-40.0],[2,true,true,null,0.0,1.0,null,null,1.01,false,100.0,0.0,40.0,0.0],[5,true,true,null,0.0,1.0,null,null,1.07,false,100.0,0.0,24.0,-6.0],[7,true,true,null,0.0,1.0,null,null,1.09,false,100.0,0.0,24.0,-6.0]]}\",\n \"dtype\": {\n \"bus\": \"uint32\",\n \"controllable\": \"bool\",\n \"in_service\": \"bool\",\n \"name\": \"object\",\n \"p_mw\": \"float64\",\n \"scaling\": \"float64\",\n \"sn_mva\": \"float64\",\n \"type\": \"object\",\n \"vm_pu\": \"float64\",\n \"slack\": \"bool\",\n \"max_p_mw\": \"float64\",\n \"min_p_mw\": \"float64\",\n \"max_q_mvar\": \"float64\",\n \"min_q_mvar\": \"float64\"\n },\n \"orient\": \"split\"\n},\"impedance\":{\n \"_module\": \"pandas.core.frame\",\n \"_class\": \"DataFrame\",\n \"_object\": \"{\\\"columns\\\":[\\\"name\\\",\\\"from_bus\\\",\\\"to_bus\\\",\\\"rft_pu\\\",\\\"xft_pu\\\",\\\"rtf_pu\\\",\\\"xtf_pu\\\",\\\"sn_mva\\\",\\\"in_service\\\"],\\\"index\\\":[],\\\"data\\\":[]}\",\n \"dtype\": {\n \"name\": \"object\",\n \"from_bus\": \"uint32\",\n \"to_bus\": \"uint32\",\n \"rft_pu\": \"float64\",\n \"xft_pu\": \"float64\",\n \"rtf_pu\": \"float64\",\n \"xtf_pu\": \"float64\",\n \"sn_mva\": \"float64\",\n \"in_service\": \"bool\"\n },\n \"orient\": \"split\"\n},\"line\":{\n \"_module\": \"pandas.core.frame\",\n \"_class\": \"DataFrame\",\n \"_object\": \"{\\\"columns\\\":[\\\"c_nf_per_km\\\",\\\"df\\\",\\\"from_bus\\\",\\\"g_us_per_km\\\",\\\"in_service\\\",\\\"length_km\\\",\\\"max_i_ka\\\",\\\"max_loading_percent\\\",\\\"name\\\",\\\"parallel\\\",\\\"r_ohm_per_km\\\",\\\"std_type\\\",\\\"to_bus\\\",\\\"type\\\",\\\"x_ohm_per_km\\\"],\\\"index\\\":[0,1,10,11,12,13,14,2,3,4,5,6,7,8,9],\\\"data\\\":[[768.484773228356175,1.0,0,0.0,true,1.0,42.339019740572553,100.0,null,1,3.532005,null,1,\\\"ol\\\",10.783732499999999],[716.088084144604636,1.0,0,0.0,true,1.0,42.339019740572553,100.0,null,1,9.8469675,null,4,\\\"ol\\\",40.649039999999999],[0.0,1.0,8,0.0,true,1.0,27479.65223546776906,100.0,null,1,0.0000137622784,null,9,\\\"ol\\\",0.00003655808],[0.0,1.0,8,0.0,true,1.0,27479.65223546776906,100.0,null,1,0.0000549928704,null,13,\\\"ol\\\",0.0001169772032],[0.0,1.0,9,0.0,true,1.0,27479.65223546776906,100.0,null,1,0.000035498112,null,10,\\\"ol\\\",0.0000830971648],[0.0,1.0,11,0.0,true,1.0,27479.65223546776906,100.0,null,1,0.0000955788288,null,12,\\\"ol\\\",0.0000864760832],[0.0,1.0,12,0.0,true,1.0,27479.65223546776906,100.0,null,1,0.0000739511552,null,13,\\\"ol\\\",0.0001505673728],[637.49305051897727,1.0,1,0.0,true,1.0,42.339019740572553,100.0,null,1,8.5639275,null,2,\\\"ol\\\",36.080032500000002],[494.857619124320308,1.0,1,0.0,true,1.0,42.339019740572553,100.0,null,1,10.5905475,null,3,\\\"ol\\\",32.134320000000002],[503.590400638278879,1.0,1,0.0,true,1.0,42.339019740572553,100.0,null,1,10.379137500000001,null,4,\\\"ol\\\",31.689630000000001],[186.299338964449987,1.0,2,0.0,true,1.0,42.339019740572553,100.0,null,1,12.2125725,null,3,\\\"ol\\\",31.1702175],[0.0,1.0,3,0.0,true,1.0,42.339019740572553,100.0,null,1,2.4330375,null,4,\\\"ol\\\",7.6745475],[0.0,1.0,5,0.0,true,1.0,27479.65223546776906,100.0,null,1,0.0000410921472,null,10,\\\"ol\\\",0.000086052096],[0.0,1.0,5,0.0,true,1.0,27479.65223546776906,100.0,null,1,0.0000531757824,null,11,\\\"ol\\\",0.0001106736384],[0.0,1.0,5,0.0,true,1.0,27479.65223546776906,100.0,null,1,0.000028619136,null,12,\\\"ol\\\",0.0000563600128]]}\",\n \"dtype\": {\n \"c_nf_per_km\": \"float64\",\n \"df\": \"float64\",\n \"from_bus\": \"uint32\",\n \"g_us_per_km\": \"float64\",\n \"in_service\": \"bool\",\n \"length_km\": \"float64\",\n \"max_i_ka\": \"float64\",\n \"max_loading_percent\": \"float64\",\n \"name\": \"object\",\n \"parallel\": \"uint32\",\n \"r_ohm_per_km\": \"float64\",\n \"std_type\": \"object\",\n \"to_bus\": \"uint32\",\n \"type\": \"object\",\n \"x_ohm_per_km\": \"float64\"\n },\n \"orient\": \"split\"\n},\"line_geodata\":{\n \"_module\": \"pandas.core.frame\",\n \"_class\": \"DataFrame\",\n \"_object\": \"{\\\"columns\\\":[\\\"coords\\\"],\\\"index\\\":[],\\\"data\\\":[]}\",\n \"dtype\": {\n \"coords\": \"object\"\n },\n \"orient\": \"split\"\n},\"load\":{\n \"_module\": \"pandas.core.frame\",\n \"_class\": \"DataFrame\",\n \"_object\": \"{\\\"columns\\\":[\\\"bus\\\",\\\"const_i_percent\\\",\\\"const_z_percent\\\",\\\"controllable\\\",\\\"in_service\\\",\\\"name\\\",\\\"p_mw\\\",\\\"q_mvar\\\",\\\"scaling\\\",\\\"sn_mva\\\",\\\"type\\\"],\\\"index\\\":[0,1,10,2,3,4,5,6,7,8,9],\\\"data\\\":[[1,0.0,0.0,false,true,null,21.699999999999999,12.699999999999999,1.0,null,null],[2,0.0,0.0,false,true,null,94.200000000000003,19.0,1.0,null,null],[13,0.0,0.0,false,true,null,14.9,5.0,1.0,null,null],[3,0.0,0.0,false,true,null,47.799999999999997,-3.9,1.0,null,null],[4,0.0,0.0,false,true,null,7.6,1.6,1.0,null,null],[5,0.0,0.0,false,true,null,11.199999999999999,7.5,1.0,null,null],[8,0.0,0.0,false,true,null,29.5,16.600000000000001,1.0,null,null],[9,0.0,0.0,false,true,null,9.0,5.8,1.0,null,null],[10,0.0,0.0,false,true,null,3.5,1.8,1.0,null,null],[11,0.0,0.0,false,true,null,6.1,1.6,1.0,null,null],[12,0.0,0.0,false,true,null,13.5,5.8,1.0,null,null]]}\",\n \"dtype\": {\n \"bus\": \"uint32\",\n \"const_i_percent\": \"float64\",\n \"const_z_percent\": \"float64\",\n \"controllable\": \"bool\",\n \"in_service\": \"bool\",\n \"name\": \"object\",\n \"p_mw\": \"float64\",\n \"q_mvar\": \"float64\",\n \"scaling\": \"float64\",\n \"sn_mva\": \"float64\",\n \"type\": \"object\"\n },\n \"orient\": \"split\"\n},\"measurement\":{\n \"_module\": \"pandas.core.frame\",\n \"_class\": \"DataFrame\",\n \"_object\": \"{\\\"columns\\\":[\\\"name\\\",\\\"measurement_type\\\",\\\"element_type\\\",\\\"element\\\",\\\"value\\\",\\\"std_dev\\\",\\\"side\\\"],\\\"index\\\":[],\\\"data\\\":[]}\",\n \"dtype\": {\n \"name\": \"object\",\n \"measurement_type\": \"object\",\n \"element_type\": \"object\",\n \"element\": \"uint32\",\n \"value\": \"float64\",\n \"std_dev\": \"float64\",\n \"side\": \"object\"\n },\n \"orient\": \"split\"\n},\"name\":\"\",\"poly_cost\":{\n \"_module\": \"pandas.core.frame\",\n \"_class\": \"DataFrame\",\n \"_object\": \"{\\\"columns\\\":[\\\"element\\\",\\\"et\\\",\\\"cp0_eur\\\",\\\"cp1_eur_per_mw\\\",\\\"cp2_eur_per_mw2\\\",\\\"cq0_eur\\\",\\\"cq1_eur_per_mvar\\\",\\\"cq2_eur_per_mvar2\\\"],\\\"index\\\":[0,1,2,3,4],\\\"data\\\":[[0.0,\\\"ext_grid\\\",0.0,20.0,0.0430293,0.0,0.0,0.0],[0.0,\\\"gen\\\",0.0,20.0,0.25,0.0,0.0,0.0],[1.0,\\\"gen\\\",0.0,40.0,0.01,0.0,0.0,0.0],[2.0,\\\"gen\\\",0.0,40.0,0.01,0.0,0.0,0.0],[3.0,\\\"gen\\\",0.0,40.0,0.01,0.0,0.0,0.0]]}\",\n \"dtype\": {\n \"element\": \"object\",\n \"et\": \"object\",\n \"cp0_eur\": \"float64\",\n \"cp1_eur_per_mw\": \"float64\",\n \"cp2_eur_per_mw2\": \"float64\",\n \"cq0_eur\": \"float64\",\n \"cq1_eur_per_mvar\": \"float64\",\n \"cq2_eur_per_mvar2\": \"float64\"\n },\n \"orient\": \"split\"\n},\"pwl_cost\":{\n \"_module\": \"pandas.core.frame\",\n \"_class\": \"DataFrame\",\n \"_object\": \"{\\\"columns\\\":[\\\"power_type\\\",\\\"element\\\",\\\"et\\\",\\\"points\\\"],\\\"index\\\":[],\\\"data\\\":[]}\",\n \"dtype\": {\n \"power_type\": \"object\",\n \"element\": \"object\",\n \"et\": \"object\",\n \"points\": \"object\"\n },\n \"orient\": \"split\"\n},\"res_bus\":{\n \"_module\": \"pandas.core.frame\",\n \"_class\": \"DataFrame\",\n \"_object\": \"{\\\"columns\\\":[\\\"vm_pu\\\",\\\"va_degree\\\",\\\"p_mw\\\",\\\"q_mvar\\\"],\\\"index\\\":[],\\\"data\\\":[]}\",\n \"dtype\": {\n \"vm_pu\": \"float64\",\n \"va_degree\": \"float64\",\n \"p_mw\": \"float64\",\n \"q_mvar\": \"float64\"\n },\n \"orient\": \"split\"\n},\"res_dcline\":{\n \"_module\": \"pandas.core.frame\",\n \"_class\": \"DataFrame\",\n \"_object\": \"{\\\"columns\\\":[\\\"p_from_mw\\\",\\\"q_from_mvar\\\",\\\"p_to_mw\\\",\\\"q_to_mvar\\\",\\\"pl_mw\\\",\\\"vm_from_pu\\\",\\\"va_from_degree\\\",\\\"vm_to_pu\\\",\\\"va_to_degree\\\"],\\\"index\\\":[],\\\"data\\\":[]}\",\n \"dtype\": {\n \"p_from_mw\": \"float64\",\n \"q_from_mvar\": \"float64\",\n \"p_to_mw\": \"float64\",\n \"q_to_mvar\": \"float64\",\n \"pl_mw\": \"float64\",\n \"vm_from_pu\": \"float64\",\n \"va_from_degree\": \"float64\",\n \"vm_to_pu\": \"float64\",\n \"va_to_degree\": \"float64\"\n },\n \"orient\": \"split\"\n},\"res_ext_grid\":{\n \"_module\": \"pandas.core.frame\",\n \"_class\": \"DataFrame\",\n \"_object\": \"{\\\"columns\\\":[\\\"p_mw\\\",\\\"q_mvar\\\"],\\\"index\\\":[0],\\\"data\\\":[[null,null]]}\",\n \"dtype\": {\n \"p_mw\": \"float64\",\n \"q_mvar\": \"float64\"\n },\n \"orient\": \"split\"\n},\"res_gen\":{\n \"_module\": \"pandas.core.frame\",\n \"_class\": \"DataFrame\",\n \"_object\": \"{\\\"columns\\\":[\\\"p_mw\\\",\\\"q_mvar\\\",\\\"va_degree\\\",\\\"vm_pu\\\"],\\\"index\\\":[0,1,2,3],\\\"data\\\":[[null,null,null,null],[null,null,null,null],[null,null,null,null],[null,null,null,null]]}\",\n \"dtype\": {\n \"p_mw\": \"float64\",\n \"q_mvar\": \"float64\",\n \"va_degree\": \"float64\",\n \"vm_pu\": \"float64\"\n },\n \"orient\": \"split\"\n},\"res_impedance\":{\n \"_module\": \"pandas.core.frame\",\n \"_class\": \"DataFrame\",\n \"_object\": \"{\\\"columns\\\":[\\\"p_from_mw\\\",\\\"q_from_mvar\\\",\\\"p_to_mw\\\",\\\"q_to_mvar\\\",\\\"pl_mw\\\",\\\"ql_mvar\\\",\\\"i_from_ka\\\",\\\"i_to_ka\\\"],\\\"index\\\":[],\\\"data\\\":[]}\",\n \"dtype\": {\n \"p_from_mw\": \"float64\",\n \"q_from_mvar\": \"float64\",\n \"p_to_mw\": \"float64\",\n \"q_to_mvar\": \"float64\",\n \"pl_mw\": \"float64\",\n \"ql_mvar\": \"float64\",\n \"i_from_ka\": \"float64\",\n \"i_to_ka\": \"float64\"\n },\n \"orient\": \"split\"\n},\"res_line\":{\n \"_module\": \"pandas.core.frame\",\n \"_class\": \"DataFrame\",\n \"_object\": \"{\\\"columns\\\":[\\\"p_from_mw\\\",\\\"q_from_mvar\\\",\\\"p_to_mw\\\",\\\"q_to_mvar\\\",\\\"pl_mw\\\",\\\"ql_mvar\\\",\\\"i_from_ka\\\",\\\"i_to_ka\\\",\\\"i_ka\\\",\\\"vm_from_pu\\\",\\\"va_from_degree\\\",\\\"vm_to_pu\\\",\\\"va_to_degree\\\",\\\"loading_percent\\\"],\\\"index\\\":[0,1,10,11,12,13,14,2,3,4,5,6,7,8,9],\\\"data\\\":[[null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null,null]]}\",\n \"dtype\": {\n \"p_from_mw\": \"float64\",\n \"q_from_mvar\": \"float64\",\n \"p_to_mw\": \"float64\",\n \"q_to_mvar\": \"float64\",\n \"pl_mw\": \"float64\",\n \"ql_mvar\": \"float64\",\n \"i_from_ka\": \"float64\",\n \"i_to_ka\": \"float64\",\n \"i_ka\": \"float64\",\n \"vm_from_pu\": \"float64\",\n \"va_from_degree\": \"float64\",\n \"vm_to_pu\": \"float64\",\n \"va_to_degree\": \"float64\",\n \"loading_percent\": \"float64\"\n },\n \"orient\": \"split\"\n},\"res_load\":{\n \"_module\": \"pandas.core.frame\",\n \"_class\": \"DataFrame\",\n \"_object\": \"{\\\"columns\\\":[\\\"p_mw\\\",\\\"q_mvar\\\"],\\\"index\\\":[0,1,10,2,3,4,5,6,7,8,9],\\\"data\\\":[[null,null],[null,null],[null,null],[null,null],[null,null],[null,null],[null,null],[null,null],[null,null],[null,null],[null,null]]}\",\n \"dtype\": {\n \"p_mw\": \"float64\",\n \"q_mvar\": \"float64\"\n },\n \"orient\": \"split\"\n},\"res_sgen\":{\n \"_module\": \"pandas.core.frame\",\n \"_class\": \"DataFrame\",\n \"_object\": \"{\\\"columns\\\":[\\\"p_mw\\\",\\\"q_mvar\\\"],\\\"index\\\":[],\\\"data\\\":[]}\",\n \"dtype\": {\n \"p_mw\": \"float64\",\n \"q_mvar\": \"float64\"\n },\n \"orient\": \"split\"\n},\"res_shunt\":{\n \"_module\": \"pandas.core.frame\",\n \"_class\": \"DataFrame\",\n \"_object\": \"{\\\"columns\\\":[\\\"p_mw\\\",\\\"q_mvar\\\",\\\"vm_pu\\\"],\\\"index\\\":[0],\\\"data\\\":[[null,null,null]]}\",\n \"dtype\": {\n \"p_mw\": \"float64\",\n \"q_mvar\": \"float64\",\n \"vm_pu\": \"float64\"\n },\n \"orient\": \"split\"\n},\"res_storage\":{\n \"_module\": \"pandas.core.frame\",\n \"_class\": \"DataFrame\",\n \"_object\": \"{\\\"columns\\\":[\\\"p_mw\\\",\\\"q_mvar\\\"],\\\"index\\\":[],\\\"data\\\":[]}\",\n \"dtype\": {\n \"p_mw\": \"float64\",\n \"q_mvar\": \"float64\"\n },\n \"orient\": \"split\"\n},\"res_trafo\":{\n \"_module\": \"pandas.core.frame\",\n \"_class\": \"DataFrame\",\n \"_object\": \"{\\\"columns\\\":[\\\"p_hv_mw\\\",\\\"q_hv_mvar\\\",\\\"p_lv_mw\\\",\\\"q_lv_mvar\\\",\\\"pl_mw\\\",\\\"ql_mvar\\\",\\\"i_hv_ka\\\",\\\"i_lv_ka\\\",\\\"vm_hv_pu\\\",\\\"va_hv_degree\\\",\\\"vm_lv_pu\\\",\\\"va_lv_degree\\\",\\\"loading_percent\\\"],\\\"index\\\":[0,1,2,3,4],\\\"data\\\":[[null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null],[null,null,null,null,null,null,null,null,null,null,null,null,null]]}\",\n \"dtype\": {\n \"p_hv_mw\": \"float64\",\n \"q_hv_mvar\": \"float64\",\n \"p_lv_mw\": \"float64\",\n \"q_lv_mvar\": \"float64\",\n \"pl_mw\": \"float64\",\n \"ql_mvar\": \"float64\",\n \"i_hv_ka\": \"float64\",\n \"i_lv_ka\": \"float64\",\n \"vm_hv_pu\": \"float64\",\n \"va_hv_degree\": \"float64\",\n \"vm_lv_pu\": \"float64\",\n \"va_lv_degree\": \"float64\",\n \"loading_percent\": \"float64\"\n },\n \"orient\": \"split\"\n},\"res_trafo3w\":{\n \"_module\": \"pandas.core.frame\",\n \"_class\": \"DataFrame\",\n \"_object\": \"{\\\"columns\\\":[\\\"p_hv_mw\\\",\\\"q_hv_mvar\\\",\\\"p_mv_mw\\\",\\\"q_mv_mvar\\\",\\\"p_lv_mw\\\",\\\"q_lv_mvar\\\",\\\"pl_mw\\\",\\\"ql_mvar\\\",\\\"i_hv_ka\\\",\\\"i_mv_ka\\\",\\\"i_lv_ka\\\",\\\"vm_hv_pu\\\",\\\"va_hv_degree\\\",\\\"vm_mv_pu\\\",\\\"va_mv_degree\\\",\\\"vm_lv_pu\\\",\\\"va_lv_degree\\\",\\\"va_internal_degree\\\",\\\"vm_internal_pu\\\",\\\"loading_percent\\\"],\\\"index\\\":[],\\\"data\\\":[]}\",\n \"dtype\": {\n \"p_hv_mw\": \"float64\",\n \"q_hv_mvar\": \"float64\",\n \"p_mv_mw\": \"float64\",\n \"q_mv_mvar\": \"float64\",\n \"p_lv_mw\": \"float64\",\n \"q_lv_mvar\": \"float64\",\n \"pl_mw\": \"float64\",\n \"ql_mvar\": \"float64\",\n \"i_hv_ka\": \"float64\",\n \"i_mv_ka\": \"float64\",\n \"i_lv_ka\": \"float64\",\n \"vm_hv_pu\": \"float64\",\n \"va_hv_degree\": \"float64\",\n \"vm_mv_pu\": \"float64\",\n \"va_mv_degree\": \"float64\",\n \"vm_lv_pu\": \"float64\",\n \"va_lv_degree\": \"float64\",\n \"va_internal_degree\": \"float64\",\n \"vm_internal_pu\": \"float64\",\n \"loading_percent\": \"float64\"\n },\n \"orient\": \"split\"\n},\"res_ward\":{\n \"_module\": \"pandas.core.frame\",\n \"_class\": \"DataFrame\",\n \"_object\": \"{\\\"columns\\\":[\\\"p_mw\\\",\\\"q_mvar\\\",\\\"vm_pu\\\"],\\\"index\\\":[],\\\"data\\\":[]}\",\n \"dtype\": {\n \"p_mw\": \"float64\",\n \"q_mvar\": \"float64\",\n \"vm_pu\": \"float64\"\n },\n \"orient\": \"split\"\n},\"res_xward\":{\n \"_module\": \"pandas.core.frame\",\n \"_class\": \"DataFrame\",\n \"_object\": \"{\\\"columns\\\":[\\\"p_mw\\\",\\\"q_mvar\\\",\\\"vm_pu\\\",\\\"va_internal_degree\\\",\\\"vm_internal_pu\\\"],\\\"index\\\":[],\\\"data\\\":[]}\",\n \"dtype\": {\n \"p_mw\": \"float64\",\n \"q_mvar\": \"float64\",\n \"vm_pu\": \"float64\",\n \"va_internal_degree\": \"float64\",\n \"vm_internal_pu\": \"float64\"\n },\n \"orient\": \"split\"\n},\"sgen\":{\n \"_module\": \"pandas.core.frame\",\n \"_class\": \"DataFrame\",\n \"_object\": \"{\\\"columns\\\":[\\\"name\\\",\\\"bus\\\",\\\"p_mw\\\",\\\"q_mvar\\\",\\\"sn_mva\\\",\\\"scaling\\\",\\\"in_service\\\",\\\"type\\\",\\\"current_source\\\"],\\\"index\\\":[],\\\"data\\\":[]}\",\n \"dtype\": {\n \"name\": \"object\",\n \"bus\": \"int64\",\n \"p_mw\": \"float64\",\n \"q_mvar\": \"float64\",\n \"sn_mva\": \"float64\",\n \"scaling\": \"float64\",\n \"in_service\": \"bool\",\n \"type\": \"object\",\n \"current_source\": \"bool\"\n },\n \"orient\": \"split\"\n},\"shunt\":{\n \"_module\": \"pandas.core.frame\",\n \"_class\": \"DataFrame\",\n \"_object\": \"{\\\"columns\\\":[\\\"bus\\\",\\\"name\\\",\\\"q_mvar\\\",\\\"p_mw\\\",\\\"vn_kv\\\",\\\"step\\\",\\\"max_step\\\",\\\"in_service\\\"],\\\"index\\\":[0],\\\"data\\\":[[8,null,-19.0,0.0,0.208,1,1,true]]}\",\n \"dtype\": {\n \"bus\": \"uint32\",\n \"name\": \"object\",\n \"q_mvar\": \"float64\",\n \"p_mw\": \"float64\",\n \"vn_kv\": \"float64\",\n \"step\": \"uint32\",\n \"max_step\": \"uint32\",\n \"in_service\": \"bool\"\n },\n \"orient\": \"split\"\n},\"sn_mva\":1.0,\"std_types\":{\n \"line\": {\n \"NAYY 4x150 SE\": {\n \"type\": \"cs\",\n \"r_ohm_per_km\": 0.208,\n \"q_mm2\": 150,\n \"x_ohm_per_km\": 0.08,\n \"c_nf_per_km\": 261.0,\n \"max_i_ka\": 0.27\n },\n \"70-AL1/11-ST1A 20.0\": {\n \"type\": \"ol\",\n \"r_ohm_per_km\": 0.4132,\n \"q_mm2\": 70,\n \"x_ohm_per_km\": 0.36,\n \"c_nf_per_km\": 9.7,\n \"max_i_ka\": 0.29\n },\n \"NA2XS2Y 1x70 RM/25 6/10 kV\": {\n \"type\": \"cs\",\n \"r_ohm_per_km\": 0.443,\n \"q_mm2\": 70,\n \"x_ohm_per_km\": 0.123,\n \"c_nf_per_km\": 280.0,\n \"max_i_ka\": 0.217\n },\n \"N2XS(FL)2Y 1x300 RM/35 64/110 kV\": {\n \"type\": \"cs\",\n \"r_ohm_per_km\": 0.06,\n \"q_mm2\": 300,\n \"x_ohm_per_km\": 0.144,\n \"c_nf_per_km\": 144.0,\n \"max_i_ka\": 0.588\n },\n \"NA2XS2Y 1x120 RM/25 6/10 kV\": {\n \"type\": \"cs\",\n \"r_ohm_per_km\": 0.253,\n \"q_mm2\": 120,\n \"x_ohm_per_km\": 0.113,\n \"c_nf_per_km\": 340.0,\n \"max_i_ka\": 0.28\n },\n \"149-AL1/24-ST1A 10.0\": {\n \"type\": \"ol\",\n \"r_ohm_per_km\": 0.194,\n \"q_mm2\": 149,\n \"x_ohm_per_km\": 0.315,\n \"c_nf_per_km\": 11.25,\n \"max_i_ka\": 0.47\n },\n \"15-AL1/3-ST1A 0.4\": {\n \"type\": \"ol\",\n \"r_ohm_per_km\": 1.8769,\n \"q_mm2\": 16,\n \"x_ohm_per_km\": 0.35,\n \"c_nf_per_km\": 11.0,\n \"max_i_ka\": 0.105\n },\n \"NA2XS2Y 1x185 RM/25 6/10 kV\": {\n \"type\": \"cs\",\n \"r_ohm_per_km\": 0.161,\n \"q_mm2\": 185,\n \"x_ohm_per_km\": 0.11,\n \"c_nf_per_km\": 406.0,\n \"max_i_ka\": 0.358\n },\n \"NA2XS2Y 1x240 RM/25 6/10 kV\": {\n \"type\": \"cs\",\n \"r_ohm_per_km\": 0.122,\n \"q_mm2\": 240,\n \"x_ohm_per_km\": 0.105,\n \"c_nf_per_km\": 456.0,\n \"max_i_ka\": 0.416\n },\n \"N2XS(FL)2Y 1x240 RM/35 64/110 kV\": {\n \"type\": \"cs\",\n \"r_ohm_per_km\": 0.075,\n \"q_mm2\": 240,\n \"x_ohm_per_km\": 0.149,\n \"c_nf_per_km\": 135.0,\n \"max_i_ka\": 0.526\n },\n \"NAYY 4x120 SE\": {\n \"type\": \"cs\",\n \"r_ohm_per_km\": 0.225,\n \"q_mm2\": 120,\n \"x_ohm_per_km\": 0.08,\n \"c_nf_per_km\": 264.0,\n \"max_i_ka\": 0.242\n },\n \"48-AL1/8-ST1A 10.0\": {\n \"type\": \"ol\",\n \"r_ohm_per_km\": 0.5939,\n \"q_mm2\": 48,\n \"x_ohm_per_km\": 0.35,\n \"c_nf_per_km\": 10.1,\n \"max_i_ka\": 0.21\n },\n \"94-AL1/15-ST1A 10.0\": {\n \"type\": \"ol\",\n \"r_ohm_per_km\": 0.306,\n \"q_mm2\": 94,\n \"x_ohm_per_km\": 0.33,\n \"c_nf_per_km\": 10.75,\n \"max_i_ka\": 0.35\n },\n \"NA2XS2Y 1x70 RM/25 12/20 kV\": {\n \"type\": \"cs\",\n \"r_ohm_per_km\": 0.443,\n \"q_mm2\": 70,\n \"x_ohm_per_km\": 0.132,\n \"c_nf_per_km\": 190.0,\n \"max_i_ka\": 0.22\n },\n \"243-AL1/39-ST1A 20.0\": {\n \"type\": \"ol\",\n \"r_ohm_per_km\": 0.1188,\n \"q_mm2\": 243,\n \"x_ohm_per_km\": 0.32,\n \"c_nf_per_km\": 11.0,\n \"max_i_ka\": 0.645\n },\n \"NA2XS2Y 1x150 RM/25 6/10 kV\": {\n \"type\": \"cs\",\n \"r_ohm_per_km\": 0.206,\n \"q_mm2\": 150,\n \"x_ohm_per_km\": 0.11,\n \"c_nf_per_km\": 360.0,\n \"max_i_ka\": 0.315\n },\n \"184-AL1/30-ST1A 110.0\": {\n \"type\": \"ol\",\n \"r_ohm_per_km\": 0.1571,\n \"q_mm2\": 184,\n \"x_ohm_per_km\": 0.4,\n \"c_nf_per_km\": 8.8,\n \"max_i_ka\": 0.535\n },\n \"149-AL1/24-ST1A 110.0\": {\n \"type\": \"ol\",\n \"r_ohm_per_km\": 0.194,\n \"q_mm2\": 149,\n \"x_ohm_per_km\": 0.41,\n \"c_nf_per_km\": 8.75,\n \"max_i_ka\": 0.47\n },\n \"NA2XS2Y 1x240 RM/25 12/20 kV\": {\n \"type\": \"cs\",\n \"r_ohm_per_km\": 0.122,\n \"q_mm2\": 240,\n \"x_ohm_per_km\": 0.112,\n \"c_nf_per_km\": 304.0,\n \"max_i_ka\": 0.421\n },\n \"122-AL1/20-ST1A 20.0\": {\n \"type\": \"ol\",\n \"r_ohm_per_km\": 0.2376,\n \"q_mm2\": 122,\n \"x_ohm_per_km\": 0.344,\n \"c_nf_per_km\": 10.3,\n \"max_i_ka\": 0.41\n },\n \"48-AL1/8-ST1A 20.0\": {\n \"type\": \"ol\",\n \"r_ohm_per_km\": 0.5939,\n \"q_mm2\": 48,\n \"x_ohm_per_km\": 0.372,\n \"c_nf_per_km\": 9.5,\n \"max_i_ka\": 0.21\n },\n \"34-AL1/6-ST1A 10.0\": {\n \"type\": \"ol\",\n \"r_ohm_per_km\": 0.8342,\n \"q_mm2\": 34,\n \"x_ohm_per_km\": 0.36,\n \"c_nf_per_km\": 9.7,\n \"max_i_ka\": 0.17\n },\n \"24-AL1/4-ST1A 0.4\": {\n \"type\": \"ol\",\n \"r_ohm_per_km\": 1.2012,\n \"q_mm2\": 24,\n \"x_ohm_per_km\": 0.335,\n \"c_nf_per_km\": 11.25,\n \"max_i_ka\": 0.14\n },\n \"184-AL1/30-ST1A 20.0\": {\n \"type\": \"ol\",\n \"r_ohm_per_km\": 0.1571,\n \"q_mm2\": 184,\n \"x_ohm_per_km\": 0.33,\n \"c_nf_per_km\": 10.75,\n \"max_i_ka\": 0.535\n },\n \"94-AL1/15-ST1A 20.0\": {\n \"type\": \"ol\",\n \"r_ohm_per_km\": 0.306,\n \"q_mm2\": 94,\n \"x_ohm_per_km\": 0.35,\n \"c_nf_per_km\": 10.0,\n \"max_i_ka\": 0.35\n },\n \"NAYY 4x50 SE\": {\n \"type\": \"cs\",\n \"r_ohm_per_km\": 0.642,\n \"q_mm2\": 50,\n \"x_ohm_per_km\": 0.083,\n \"c_nf_per_km\": 210.0,\n \"max_i_ka\": 0.142\n },\n \"490-AL1/64-ST1A 380.0\": {\n \"type\": \"ol\",\n \"r_ohm_per_km\": 0.059,\n \"q_mm2\": 490,\n \"x_ohm_per_km\": 0.253,\n \"c_nf_per_km\": 11.0,\n \"max_i_ka\": 0.96\n },\n \"48-AL1/8-ST1A 0.4\": {\n \"type\": \"ol\",\n \"r_ohm_per_km\": 0.5939,\n \"q_mm2\": 48,\n \"x_ohm_per_km\": 0.3,\n \"c_nf_per_km\": 12.2,\n \"max_i_ka\": 0.21\n },\n \"NA2XS2Y 1x95 RM/25 6/10 kV\": {\n \"type\": \"cs\",\n \"r_ohm_per_km\": 0.313,\n \"q_mm2\": 95,\n \"x_ohm_per_km\": 0.123,\n \"c_nf_per_km\": 315.0,\n \"max_i_ka\": 0.249\n },\n \"NA2XS2Y 1x120 RM/25 12/20 kV\": {\n \"type\": \"cs\",\n \"r_ohm_per_km\": 0.253,\n \"q_mm2\": 120,\n \"x_ohm_per_km\": 0.119,\n \"c_nf_per_km\": 230.0,\n \"max_i_ka\": 0.283\n },\n \"34-AL1/6-ST1A 20.0\": {\n \"type\": \"ol\",\n \"r_ohm_per_km\": 0.8342,\n \"q_mm2\": 34,\n \"x_ohm_per_km\": 0.382,\n \"c_nf_per_km\": 9.15,\n \"max_i_ka\": 0.17\n },\n \"94-AL1/15-ST1A 0.4\": {\n \"type\": \"ol\",\n \"r_ohm_per_km\": 0.306,\n \"q_mm2\": 94,\n \"x_ohm_per_km\": 0.29,\n \"c_nf_per_km\": 13.2,\n \"max_i_ka\": 0.35\n },\n \"NA2XS2Y 1x185 RM/25 12/20 kV\": {\n \"type\": \"cs\",\n \"r_ohm_per_km\": 0.161,\n \"q_mm2\": 185,\n \"x_ohm_per_km\": 0.117,\n \"c_nf_per_km\": 273.0,\n \"max_i_ka\": 0.362\n },\n \"NA2XS2Y 1x150 RM/25 12/20 kV\": {\n \"type\": \"cs\",\n \"r_ohm_per_km\": 0.206,\n \"q_mm2\": 150,\n \"x_ohm_per_km\": 0.116,\n \"c_nf_per_km\": 250.0,\n \"max_i_ka\": 0.319\n },\n \"243-AL1/39-ST1A 110.0\": {\n \"type\": \"ol\",\n \"r_ohm_per_km\": 0.1188,\n \"q_mm2\": 243,\n \"x_ohm_per_km\": 0.39,\n \"c_nf_per_km\": 9.0,\n \"max_i_ka\": 0.645\n },\n \"490-AL1/64-ST1A 220.0\": {\n \"type\": \"ol\",\n \"r_ohm_per_km\": 0.059,\n \"q_mm2\": 490,\n \"x_ohm_per_km\": 0.285,\n \"c_nf_per_km\": 10.0,\n \"max_i_ka\": 0.96\n },\n \"N2XS(FL)2Y 1x185 RM/35 64/110 kV\": {\n \"type\": \"cs\",\n \"r_ohm_per_km\": 0.099,\n \"q_mm2\": 185,\n \"x_ohm_per_km\": 0.156,\n \"c_nf_per_km\": 125.0,\n \"max_i_ka\": 0.457\n },\n \"N2XS(FL)2Y 1x120 RM/35 64/110 kV\": {\n \"type\": \"cs\",\n \"r_ohm_per_km\": 0.153,\n \"q_mm2\": 120,\n \"x_ohm_per_km\": 0.166,\n \"c_nf_per_km\": 112.0,\n \"max_i_ka\": 0.366\n },\n \"NA2XS2Y 1x95 RM/25 12/20 kV\": {\n \"type\": \"cs\",\n \"r_ohm_per_km\": 0.313,\n \"q_mm2\": 95,\n \"x_ohm_per_km\": 0.132,\n \"c_nf_per_km\": 216.0,\n \"max_i_ka\": 0.252\n },\n \"122-AL1/20-ST1A 10.0\": {\n \"type\": \"ol\",\n \"r_ohm_per_km\": 0.2376,\n \"q_mm2\": 122,\n \"x_ohm_per_km\": 0.323,\n \"c_nf_per_km\": 11.1,\n \"max_i_ka\": 0.41\n },\n \"149-AL1/24-ST1A 20.0\": {\n \"type\": \"ol\",\n \"r_ohm_per_km\": 0.194,\n \"q_mm2\": 149,\n \"x_ohm_per_km\": 0.337,\n \"c_nf_per_km\": 10.5,\n \"max_i_ka\": 0.47\n },\n \"70-AL1/11-ST1A 10.0\": {\n \"type\": \"ol\",\n \"r_ohm_per_km\": 0.4132,\n \"q_mm2\": 70,\n \"x_ohm_per_km\": 0.339,\n \"c_nf_per_km\": 10.4,\n \"max_i_ka\": 0.29\n },\n \"305-AL1/39-ST1A 110.0\": {\n \"type\": \"ol\",\n \"r_ohm_per_km\": 0.0949,\n \"q_mm2\": 305,\n \"x_ohm_per_km\": 0.38,\n \"c_nf_per_km\": 9.2,\n \"max_i_ka\": 0.74\n }\n },\n \"trafo\": {\n \"0.4 MVA 20/0.4 kV\": {\n \"shift_degree\": 150,\n \"vector_group\": \"Dyn5\",\n \"vn_hv_kv\": 20.0,\n \"pfe_kw\": 1.35,\n \"i0_percent\": 0.3375,\n \"vn_lv_kv\": 0.4,\n \"sn_mva\": 0.4,\n \"tap_step_degree\": 0,\n \"tap_neutral\": 0,\n \"tap_min\": -2,\n \"vkr_percent\": 1.425,\n \"tap_step_percent\": 2.5,\n \"tap_side\": \"hv\",\n \"tap_phase_shifter\": false,\n \"tap_max\": 2,\n \"vk_percent\": 6.0\n },\n \"63 MVA 110/20 kV v1.4.3 and older\": {\n \"shift_degree\": 150,\n \"vector_group\": \"YNd5\",\n \"vn_hv_kv\": 110.0,\n \"pfe_kw\": 33.0,\n \"i0_percent\": 0.086,\n \"vn_lv_kv\": 20.0,\n \"sn_mva\": 63.0,\n \"tap_step_degree\": 0,\n \"tap_neutral\": 0,\n \"tap_min\": -9,\n \"vkr_percent\": 0.322,\n \"tap_step_percent\": 1.5,\n \"tap_side\": \"hv\",\n \"tap_phase_shifter\": false,\n \"tap_max\": 9,\n \"vk_percent\": 11.2\n },\n \"63 MVA 110/10 kV v1.4.3 and older\": {\n \"shift_degree\": 150,\n \"vector_group\": \"YNd5\",\n \"vn_hv_kv\": 110.0,\n \"pfe_kw\": 31.51,\n \"i0_percent\": 0.078,\n \"vn_lv_kv\": 10.0,\n \"sn_mva\": 63.0,\n \"tap_step_degree\": 0,\n \"tap_neutral\": 0,\n \"tap_min\": -9,\n \"vkr_percent\": 0.31,\n \"tap_step_percent\": 1.5,\n \"tap_side\": \"hv\",\n \"tap_phase_shifter\": false,\n \"tap_max\": 9,\n \"vk_percent\": 10.04\n },\n \"25 MVA 110/20 kV v1.4.3 and older\": {\n \"shift_degree\": 150,\n \"vector_group\": \"YNd5\",\n \"vn_hv_kv\": 110.0,\n \"pfe_kw\": 29.0,\n \"i0_percent\": 0.071,\n \"vn_lv_kv\": 20.0,\n \"sn_mva\": 25.0,\n \"tap_step_degree\": 0,\n \"tap_neutral\": 0,\n \"tap_min\": -9,\n \"vkr_percent\": 0.282,\n \"tap_step_percent\": 1.5,\n \"tap_side\": \"hv\",\n \"tap_phase_shifter\": false,\n \"tap_max\": 9,\n \"vk_percent\": 11.2\n },\n \"40 MVA 110/20 kV v1.4.3 and older\": {\n \"shift_degree\": 150,\n \"vector_group\": \"YNd5\",\n \"vn_hv_kv\": 110.0,\n \"pfe_kw\": 31.0,\n \"i0_percent\": 0.08,\n \"vn_lv_kv\": 20.0,\n \"sn_mva\": 40.0,\n \"tap_step_degree\": 0,\n \"tap_neutral\": 0,\n \"tap_min\": -9,\n \"vkr_percent\": 0.302,\n \"tap_step_percent\": 1.5,\n \"tap_side\": \"hv\",\n \"tap_phase_shifter\": false,\n \"tap_max\": 9,\n \"vk_percent\": 11.2\n },\n \"0.25 MVA 20/0.4 kV\": {\n \"shift_degree\": 150,\n \"vector_group\": \"Yzn5\",\n \"vn_hv_kv\": 20.0,\n \"pfe_kw\": 0.8,\n \"i0_percent\": 0.32,\n \"vn_lv_kv\": 0.4,\n \"sn_mva\": 0.25,\n \"tap_step_degree\": 0,\n \"tap_neutral\": 0,\n \"tap_min\": -2,\n \"vkr_percent\": 1.44,\n \"tap_step_percent\": 2.5,\n \"tap_side\": \"hv\",\n \"tap_phase_shifter\": false,\n \"tap_max\": 2,\n \"vk_percent\": 6.0\n },\n \"25 MVA 110/10 kV v1.4.3 and older\": {\n \"shift_degree\": 150,\n \"vector_group\": \"YNd5\",\n \"vn_hv_kv\": 110.0,\n \"pfe_kw\": 28.51,\n \"i0_percent\": 0.073,\n \"vn_lv_kv\": 10.0,\n \"sn_mva\": 25.0,\n \"tap_step_degree\": 0,\n \"tap_neutral\": 0,\n \"tap_min\": -9,\n \"vkr_percent\": 0.276,\n \"tap_step_percent\": 1.5,\n \"tap_side\": \"hv\",\n \"tap_phase_shifter\": false,\n \"tap_max\": 9,\n \"vk_percent\": 10.04\n },\n \"0.25 MVA 10/0.4 kV\": {\n \"shift_degree\": 150,\n \"vector_group\": \"Dyn5\",\n \"vn_hv_kv\": 10.0,\n \"pfe_kw\": 0.6,\n \"i0_percent\": 0.24,\n \"vn_lv_kv\": 0.4,\n \"sn_mva\": 0.25,\n \"tap_step_degree\": 0,\n \"tap_neutral\": 0,\n \"tap_min\": -2,\n \"vkr_percent\": 1.2,\n \"tap_step_percent\": 2.5,\n \"tap_side\": \"hv\",\n \"tap_phase_shifter\": false,\n \"tap_max\": 2,\n \"vk_percent\": 4.0\n },\n \"160 MVA 380/110 kV\": {\n \"shift_degree\": 0,\n \"vector_group\": \"Yy0\",\n \"vn_hv_kv\": 380.0,\n \"pfe_kw\": 60.0,\n \"i0_percent\": 0.06,\n \"vn_lv_kv\": 110.0,\n \"sn_mva\": 160.0,\n \"tap_step_degree\": 0,\n \"tap_neutral\": 0,\n \"tap_min\": -9,\n \"vkr_percent\": 0.25,\n \"tap_step_percent\": 1.5,\n \"tap_side\": \"hv\",\n \"tap_phase_shifter\": false,\n \"tap_max\": 9,\n \"vk_percent\": 12.2\n },\n \"63 MVA 110/10 kV\": {\n \"shift_degree\": 150,\n \"vector_group\": \"YNd5\",\n \"vn_hv_kv\": 110.0,\n \"pfe_kw\": 22.0,\n \"i0_percent\": 0.04,\n \"vn_lv_kv\": 10.0,\n \"sn_mva\": 63.0,\n \"tap_step_degree\": 0,\n \"tap_neutral\": 0,\n \"tap_min\": -9,\n \"vkr_percent\": 0.32,\n \"tap_step_percent\": 1.5,\n \"tap_side\": \"hv\",\n \"tap_phase_shifter\": false,\n \"tap_max\": 9,\n \"vk_percent\": 18.0\n },\n \"0.63 MVA 20/0.4 kV\": {\n \"shift_degree\": 150,\n \"vector_group\": \"Dyn5\",\n \"vn_hv_kv\": 20.0,\n \"pfe_kw\": 1.65,\n \"i0_percent\": 0.2619,\n \"vn_lv_kv\": 0.4,\n \"sn_mva\": 0.63,\n \"tap_step_degree\": 0,\n \"tap_neutral\": 0,\n \"tap_min\": -2,\n \"vkr_percent\": 1.206,\n \"tap_step_percent\": 2.5,\n \"tap_side\": \"hv\",\n \"tap_phase_shifter\": false,\n \"tap_max\": 2,\n \"vk_percent\": 6.0\n },\n \"0.4 MVA 10/0.4 kV\": {\n \"shift_degree\": 150,\n \"vector_group\": \"Dyn5\",\n \"vn_hv_kv\": 10.0,\n \"pfe_kw\": 0.95,\n \"i0_percent\": 0.2375,\n \"vn_lv_kv\": 0.4,\n \"sn_mva\": 0.4,\n \"tap_step_degree\": 0,\n \"tap_neutral\": 0,\n \"tap_min\": -2,\n \"vkr_percent\": 1.325,\n \"tap_step_percent\": 2.5,\n \"tap_side\": \"hv\",\n \"tap_phase_shifter\": false,\n \"tap_max\": 2,\n \"vk_percent\": 4.0\n },\n \"0.63 MVA 10/0.4 kV\": {\n \"shift_degree\": 150,\n \"vector_group\": \"Dyn5\",\n \"vn_hv_kv\": 10.0,\n \"pfe_kw\": 1.18,\n \"i0_percent\": 0.1873,\n \"vn_lv_kv\": 0.4,\n \"sn_mva\": 0.63,\n \"tap_step_degree\": 0,\n \"tap_neutral\": 0,\n \"tap_min\": -2,\n \"vkr_percent\": 1.0794,\n \"tap_step_percent\": 2.5,\n \"tap_side\": \"hv\",\n \"tap_phase_shifter\": false,\n \"tap_max\": 2,\n \"vk_percent\": 4.0\n },\n \"63 MVA 110/20 kV\": {\n \"shift_degree\": 150,\n \"vector_group\": \"YNd5\",\n \"vn_hv_kv\": 110.0,\n \"pfe_kw\": 22.0,\n \"i0_percent\": 0.04,\n \"vn_lv_kv\": 20.0,\n \"sn_mva\": 63.0,\n \"tap_step_degree\": 0,\n \"tap_neutral\": 0,\n \"tap_min\": -9,\n \"vkr_percent\": 0.32,\n \"tap_step_percent\": 1.5,\n \"tap_side\": \"hv\",\n \"tap_phase_shifter\": false,\n \"tap_max\": 9,\n \"vk_percent\": 18.0\n },\n \"100 MVA 220/110 kV\": {\n \"shift_degree\": 0,\n \"vector_group\": \"Yy0\",\n \"vn_hv_kv\": 220.0,\n \"pfe_kw\": 55.0,\n \"i0_percent\": 0.06,\n \"vn_lv_kv\": 110.0,\n \"sn_mva\": 100.0,\n \"tap_step_degree\": 0,\n \"tap_neutral\": 0,\n \"tap_min\": -9,\n \"vkr_percent\": 0.26,\n \"tap_step_percent\": 1.5,\n \"tap_side\": \"hv\",\n \"tap_phase_shifter\": false,\n \"tap_max\": 9,\n \"vk_percent\": 12.0\n },\n \"25 MVA 110/10 kV\": {\n \"shift_degree\": 150,\n \"vector_group\": \"YNd5\",\n \"vn_hv_kv\": 110.0,\n \"pfe_kw\": 14.0,\n \"i0_percent\": 0.07,\n \"vn_lv_kv\": 10.0,\n \"sn_mva\": 25.0,\n \"tap_step_degree\": 0,\n \"tap_neutral\": 0,\n \"tap_min\": -9,\n \"vkr_percent\": 0.41,\n \"tap_step_percent\": 1.5,\n \"tap_side\": \"hv\",\n \"tap_phase_shifter\": false,\n \"tap_max\": 9,\n \"vk_percent\": 12.0\n },\n \"40 MVA 110/20 kV\": {\n \"shift_degree\": 150,\n \"vector_group\": \"YNd5\",\n \"vn_hv_kv\": 110.0,\n \"pfe_kw\": 18.0,\n \"i0_percent\": 0.05,\n \"vn_lv_kv\": 20.0,\n \"sn_mva\": 40.0,\n \"tap_step_degree\": 0,\n \"tap_neutral\": 0,\n \"tap_min\": -9,\n \"vkr_percent\": 0.34,\n \"tap_step_percent\": 1.5,\n \"tap_side\": \"hv\",\n \"tap_phase_shifter\": false,\n \"tap_max\": 9,\n \"vk_percent\": 16.2\n },\n \"40 MVA 110/10 kV v1.4.3 and older\": {\n \"shift_degree\": 150,\n \"vector_group\": \"YNd5\",\n \"vn_hv_kv\": 110.0,\n \"pfe_kw\": 30.45,\n \"i0_percent\": 0.076,\n \"vn_lv_kv\": 10.0,\n \"sn_mva\": 40.0,\n \"tap_step_degree\": 0,\n \"tap_neutral\": 0,\n \"tap_min\": -9,\n \"vkr_percent\": 0.295,\n \"tap_step_percent\": 1.5,\n \"tap_side\": \"hv\",\n \"tap_phase_shifter\": false,\n \"tap_max\": 9,\n \"vk_percent\": 10.04\n },\n \"25 MVA 110/20 kV\": {\n \"shift_degree\": 150,\n \"vector_group\": \"YNd5\",\n \"vn_hv_kv\": 110.0,\n \"pfe_kw\": 14.0,\n \"i0_percent\": 0.07,\n \"vn_lv_kv\": 20.0,\n \"sn_mva\": 25.0,\n \"tap_step_degree\": 0,\n \"tap_neutral\": 0,\n \"tap_min\": -9,\n \"vkr_percent\": 0.41,\n \"tap_step_percent\": 1.5,\n \"tap_side\": \"hv\",\n \"tap_phase_shifter\": false,\n \"tap_max\": 9,\n \"vk_percent\": 12.0\n },\n \"40 MVA 110/10 kV\": {\n \"shift_degree\": 150,\n \"vector_group\": \"YNd5\",\n \"vn_hv_kv\": 110.0,\n \"pfe_kw\": 18.0,\n \"i0_percent\": 0.05,\n \"vn_lv_kv\": 10.0,\n \"sn_mva\": 40.0,\n \"tap_step_degree\": 0,\n \"tap_neutral\": 0,\n \"tap_min\": -9,\n \"vkr_percent\": 0.34,\n \"tap_step_percent\": 1.5,\n \"tap_side\": \"hv\",\n \"tap_phase_shifter\": false,\n \"tap_max\": 9,\n \"vk_percent\": 16.2\n }\n },\n \"trafo3w\": {\n \"63/25/38 MVA 110/10/10 kV\": {\n \"vector_group\": \"YN0yn0yn0\",\n \"vn_mv_kv\": 10,\n \"vn_lv_kv\": 10,\n \"shift_lv_degree\": 0,\n \"shift_mv_degree\": 0,\n \"pfe_kw\": 35,\n \"vn_hv_kv\": 110,\n \"i0_percent\": 0.89,\n \"sn_lv_mva\": 38.0,\n \"sn_hv_mva\": 63.0,\n \"sn_mv_mva\": 25.0,\n \"vkr_lv_percent\": 0.35,\n \"tap_neutral\": 0,\n \"tap_min\": -10,\n \"vk_mv_percent\": 10.4,\n \"vkr_hv_percent\": 0.28,\n \"vk_lv_percent\": 10.4,\n \"tap_max\": 10,\n \"vkr_mv_percent\": 0.32,\n \"tap_step_percent\": 1.2,\n \"tap_side\": \"hv\",\n \"vk_hv_percent\": 10.4\n },\n \"63/25/38 MVA 110/20/10 kV\": {\n \"vector_group\": \"YN0yn0yn0\",\n \"vn_mv_kv\": 20,\n \"vn_lv_kv\": 10,\n \"shift_lv_degree\": 0,\n \"shift_mv_degree\": 0,\n \"pfe_kw\": 35,\n \"vn_hv_kv\": 110,\n \"i0_percent\": 0.89,\n \"sn_lv_mva\": 38.0,\n \"sn_hv_mva\": 63.0,\n \"sn_mv_mva\": 25.0,\n \"vkr_lv_percent\": 0.35,\n \"tap_neutral\": 0,\n \"tap_min\": -10,\n \"vk_mv_percent\": 10.4,\n \"vkr_hv_percent\": 0.28,\n \"vk_lv_percent\": 10.4,\n \"tap_max\": 10,\n \"vkr_mv_percent\": 0.32,\n \"tap_step_percent\": 1.2,\n \"tap_side\": \"hv\",\n \"vk_hv_percent\": 10.4\n }\n }\n},\"storage\":{\n \"_module\": \"pandas.core.frame\",\n \"_class\": \"DataFrame\",\n \"_object\": \"{\\\"columns\\\":[\\\"name\\\",\\\"bus\\\",\\\"p_mw\\\",\\\"q_mvar\\\",\\\"sn_mva\\\",\\\"soc_percent\\\",\\\"min_e_mwh\\\",\\\"max_e_mwh\\\",\\\"scaling\\\",\\\"in_service\\\",\\\"type\\\"],\\\"index\\\":[],\\\"data\\\":[]}\",\n \"dtype\": {\n \"name\": \"object\",\n \"bus\": \"int64\",\n \"p_mw\": \"float64\",\n \"q_mvar\": \"float64\",\n \"sn_mva\": \"float64\",\n \"soc_percent\": \"float64\",\n \"min_e_mwh\": \"float64\",\n \"max_e_mwh\": \"float64\",\n \"scaling\": \"float64\",\n \"in_service\": \"bool\",\n \"type\": \"object\"\n },\n \"orient\": \"split\"\n},\"switch\":{\n \"_module\": \"pandas.core.frame\",\n \"_class\": \"DataFrame\",\n \"_object\": \"{\\\"columns\\\":[\\\"bus\\\",\\\"element\\\",\\\"et\\\",\\\"type\\\",\\\"closed\\\",\\\"name\\\",\\\"z_ohm\\\"],\\\"index\\\":[],\\\"data\\\":[]}\",\n \"dtype\": {\n \"bus\": \"int64\",\n \"element\": \"int64\",\n \"et\": \"object\",\n \"type\": \"object\",\n \"closed\": \"bool\",\n \"name\": \"object\",\n \"z_ohm\": \"float64\"\n },\n \"orient\": \"split\"\n},\"trafo\":{\n \"_module\": \"pandas.core.frame\",\n \"_class\": \"DataFrame\",\n \"_object\": \"{\\\"columns\\\":[\\\"df\\\",\\\"hv_bus\\\",\\\"i0_percent\\\",\\\"in_service\\\",\\\"lv_bus\\\",\\\"max_loading_percent\\\",\\\"name\\\",\\\"parallel\\\",\\\"pfe_kw\\\",\\\"shift_degree\\\",\\\"sn_mva\\\",\\\"std_type\\\",\\\"tap_max\\\",\\\"tap_neutral\\\",\\\"tap_min\\\",\\\"tap_phase_shifter\\\",\\\"tap_pos\\\",\\\"tap_side\\\",\\\"tap_step_degree\\\",\\\"tap_step_percent\\\",\\\"vn_hv_kv\\\",\\\"vn_lv_kv\\\",\\\"vk_percent\\\",\\\"vkr_percent\\\"],\\\"index\\\":[0,1,2,3,4],\\\"data\\\":[[1.0,3,0.0,true,6,100.0,null,1,0.0,0.0,9900.0,null,null,0.0,null,false,-1.0,\\\"hv\\\",null,2.2,135.0,14.0,2070.288000000000011,0.0],[1.0,3,0.0,true,8,100.0,null,1,0.0,0.0,9900.0,null,null,0.0,null,false,-1.0,\\\"hv\\\",null,3.1,135.0,0.208,5506.181999999999789,0.0],[1.0,4,0.0,true,5,100.0,null,1,0.0,0.0,9900.0,null,null,0.0,null,false,-1.0,\\\"hv\\\",null,6.8,135.0,0.208,2494.998000000000047,0.0],[1.0,6,0.0,true,7,100.0,null,1,0.0,0.0,9900.0,null,null,null,null,false,null,null,null,null,14.0,12.0,1743.884999999999991,0.0],[1.0,6,0.0,true,8,100.0,null,1,0.0,0.0,9900.0,null,null,null,null,false,null,null,null,null,14.0,0.208,1089.098999999999933,0.0]]}\",\n \"dtype\": {\n \"df\": \"float64\",\n \"hv_bus\": \"uint32\",\n \"i0_percent\": \"float64\",\n \"in_service\": \"bool\",\n \"lv_bus\": \"uint32\",\n \"max_loading_percent\": \"float64\",\n \"name\": \"object\",\n \"parallel\": \"uint32\",\n \"pfe_kw\": \"float64\",\n \"shift_degree\": \"float64\",\n \"sn_mva\": \"float64\",\n \"std_type\": \"object\",\n \"tap_max\": \"float64\",\n \"tap_neutral\": \"float64\",\n \"tap_min\": \"float64\",\n \"tap_phase_shifter\": \"bool\",\n \"tap_pos\": \"float64\",\n \"tap_side\": \"object\",\n \"tap_step_degree\": \"float64\",\n \"tap_step_percent\": \"float64\",\n \"vn_hv_kv\": \"float64\",\n \"vn_lv_kv\": \"float64\",\n \"vk_percent\": \"float64\",\n \"vkr_percent\": \"float64\"\n },\n \"orient\": \"split\"\n},\"trafo3w\":{\n \"_module\": \"pandas.core.frame\",\n \"_class\": \"DataFrame\",\n \"_object\": \"{\\\"columns\\\":[\\\"name\\\",\\\"std_type\\\",\\\"hv_bus\\\",\\\"mv_bus\\\",\\\"lv_bus\\\",\\\"sn_hv_mva\\\",\\\"sn_mv_mva\\\",\\\"sn_lv_mva\\\",\\\"vn_hv_kv\\\",\\\"vn_mv_kv\\\",\\\"vn_lv_kv\\\",\\\"vk_hv_percent\\\",\\\"vk_mv_percent\\\",\\\"vk_lv_percent\\\",\\\"vkr_hv_percent\\\",\\\"vkr_mv_percent\\\",\\\"vkr_lv_percent\\\",\\\"pfe_kw\\\",\\\"i0_percent\\\",\\\"shift_mv_degree\\\",\\\"shift_lv_degree\\\",\\\"tap_side\\\",\\\"tap_neutral\\\",\\\"tap_min\\\",\\\"tap_max\\\",\\\"tap_step_percent\\\",\\\"tap_step_degree\\\",\\\"tap_pos\\\",\\\"tap_at_star_point\\\",\\\"in_service\\\"],\\\"index\\\":[],\\\"data\\\":[]}\",\n \"dtype\": {\n \"name\": \"object\",\n \"std_type\": \"object\",\n \"hv_bus\": \"uint32\",\n \"mv_bus\": \"uint32\",\n \"lv_bus\": \"uint32\",\n \"sn_hv_mva\": \"float64\",\n \"sn_mv_mva\": \"float64\",\n \"sn_lv_mva\": \"float64\",\n \"vn_hv_kv\": \"float64\",\n \"vn_mv_kv\": \"float64\",\n \"vn_lv_kv\": \"float64\",\n \"vk_hv_percent\": \"float64\",\n \"vk_mv_percent\": \"float64\",\n \"vk_lv_percent\": \"float64\",\n \"vkr_hv_percent\": \"float64\",\n \"vkr_mv_percent\": \"float64\",\n \"vkr_lv_percent\": \"float64\",\n \"pfe_kw\": \"float64\",\n \"i0_percent\": \"float64\",\n \"shift_mv_degree\": \"float64\",\n \"shift_lv_degree\": \"float64\",\n \"tap_side\": \"object\",\n \"tap_neutral\": \"int32\",\n \"tap_min\": \"int32\",\n \"tap_max\": \"int32\",\n \"tap_step_percent\": \"float64\",\n \"tap_step_degree\": \"float64\",\n \"tap_pos\": \"int32\",\n \"tap_at_star_point\": \"bool\",\n \"in_service\": \"bool\"\n },\n \"orient\": \"split\"\n},\"user_pf_options\":{},\"version\":\"2.0.1\",\"ward\":{\n \"_module\": \"pandas.core.frame\",\n \"_class\": \"DataFrame\",\n \"_object\": \"{\\\"columns\\\":[\\\"name\\\",\\\"bus\\\",\\\"ps_mw\\\",\\\"qs_mvar\\\",\\\"qz_mvar\\\",\\\"pz_mw\\\",\\\"in_service\\\"],\\\"index\\\":[],\\\"data\\\":[]}\",\n \"dtype\": {\n \"name\": \"object\",\n \"bus\": \"uint32\",\n \"ps_mw\": \"float64\",\n \"qs_mvar\": \"float64\",\n \"qz_mvar\": \"float64\",\n \"pz_mw\": \"float64\",\n \"in_service\": \"bool\"\n },\n \"orient\": \"split\"\n},\"xward\":{\n \"_module\": \"pandas.core.frame\",\n \"_class\": \"DataFrame\",\n \"_object\": \"{\\\"columns\\\":[\\\"name\\\",\\\"bus\\\",\\\"ps_mw\\\",\\\"qs_mvar\\\",\\\"qz_mvar\\\",\\\"pz_mw\\\",\\\"r_ohm\\\",\\\"x_ohm\\\",\\\"vm_pu\\\",\\\"in_service\\\"],\\\"index\\\":[],\\\"data\\\":[]}\",\n \"dtype\": {\n \"name\": \"object\",\n \"bus\": \"uint32\",\n \"ps_mw\": \"float64\",\n \"qs_mvar\": \"float64\",\n \"qz_mvar\": \"float64\",\n \"pz_mw\": \"float64\",\n \"r_ohm\": \"float64\",\n \"x_ohm\": \"float64\",\n \"vm_pu\": \"float64\",\n \"in_service\": \"bool\"\n },\n \"orient\": \"split\"\n}}\n" +} diff --git a/grid2op/data_test/multimix/case14_002/prods_charac.csv b/grid2op/data_test/multimix/case14_002/prods_charac.csv index 21c8943e5..f27dff8db 100644 --- a/grid2op/data_test/multimix/case14_002/prods_charac.csv +++ b/grid2op/data_test/multimix/case14_002/prods_charac.csv @@ -1,6 +1,6 @@ Pmax,Pmin,name,type,bus,max_ramp_up,max_ramp_down,min_up_time,min_down_time,marginal_cost,shut_down_cost,start_cost,x,y,V 150,0.0,gen_1_0,nuclear,1,5,5,96,96,40,10,20,180,10,142.1 200,0.0,gen_2_1,thermal,2,10,10,4,4,70,1,2,646,10,142.1 -70,0.0,gen_5_2,wind,5,0,0,0,0,0,0,0,216,334,22.0 -50,0.0,gen_7_3,solar,7,0,0,0,0,0,0,0,718,280,13.2 +70,0.0,gen_5_2,wind,5,0,0,0,0,0,0,0,216,334,0.208 +50,0.0,gen_7_3,solar,7,0,0,0,0,0,0,0,718,280,12.0 300,0.0,gen_0_4,thermal,0,10,10,4,4,70,1,2,0,199,142.1 \ No newline at end of file diff --git a/grid2op/gym_compat/box_gym_obsspace.py b/grid2op/gym_compat/box_gym_obsspace.py index 76879ef9e..eefe71893 100644 --- a/grid2op/gym_compat/box_gym_obsspace.py +++ b/grid2op/gym_compat/box_gym_obsspace.py @@ -215,9 +215,11 @@ def __init__( ob_sp = grid2op_observation_space ob_sp_cls = type(grid2op_observation_space) - tol_redisp = ( - ob_sp.obs_env._tol_poly - ) # add to gen_p otherwise ... well it can crash + # add to gen_p otherwise ... well it can crash + if ob_sp.obs_env is not None: + tol_redisp = ob_sp.obs_env._tol_poly + else: + tol_redisp = 1e-2 extra_for_losses = _compute_extra_power_for_losses(ob_sp) self._dict_properties = { diff --git a/grid2op/gym_compat/gym_act_space.py b/grid2op/gym_compat/gym_act_space.py index 94bf2ff0f..984de4127 100644 --- a/grid2op/gym_compat/gym_act_space.py +++ b/grid2op/gym_compat/gym_act_space.py @@ -126,24 +126,35 @@ def __init__(self, env, converter=None, dict_variables=None): env, (Environment, MultiMixEnvironment, BaseMultiProcessEnvironment) ): # action_space is an environment - self.initial_act_space = env.action_space - self._init_env = env + # self.initial_act_space = env.action_space + # self._init_env = env + self._template_act = env.action_space() + self._converter = None + self.__is_converter = False elif isinstance(env, ActionSpace) and converter is None: warnings.warn( "It is now deprecated to initialize an Converter with an " "action space. Please use an environment instead." ) - self.initial_act_space = env - self._init_env = None + self._converter = None + self._template_act = env() + self.__is_converter = False + elif isinstance(env, type(self)): + self._template_act = env._template_act.copy() + self._converter = env._converter + self.__is_converter = env.__is_converter else: raise RuntimeError( "GymActionSpace must be created with an Environment or an ActionSpace (or a Converter)" ) dict_ = {} + # TODO Make sure it works well ! if converter is not None and isinstance(converter, Converter): # a converter allows to ... convert the data so they have specific gym space - self.initial_act_space = converter + # self.initial_act_space = converter + self._converter = converter + self._template_act = converter.init_action_space() dict_ = converter.get_gym_dict(type(self)) self.__is_converter = True elif converter is not None: @@ -155,7 +166,7 @@ def __init__(self, env, converter=None, dict_variables=None): ) else: self._fill_dict_act_space( - dict_, self.initial_act_space, dict_variables=dict_variables + dict_, dict_variables=dict_variables ) dict_ = self._fix_dict_keys(dict_) self.__is_converter = False @@ -194,11 +205,11 @@ def reencode_space(self, key, fun): If an attribute has been ignored, for example by :func`GymEnv.keep_only_obs_attr` or and is now present here, it will be re added in the final observation """ - if self._init_env is None: - raise RuntimeError( - "Impossible to reencode a space that has been initialized with an " - "action space as input. Please provide a valid" - ) + # if self._init_env is None: + # raise RuntimeError( + # "Impossible to reencode a space that has been initialized with an " + # "action space as input. Please provide a valid" + # ) if self.__is_converter: raise RuntimeError( "Impossible to reencode a space that is a converter space." @@ -224,13 +235,15 @@ def reencode_space(self, key, fun): else: raise RuntimeError(f"Impossible to find key {key} in your action space") my_dict[key2] = fun - res = type(self)(env=self._init_env, dict_variables=my_dict) + res = type(self)(env=self, dict_variables=my_dict) return res - def _fill_dict_act_space(self, dict_, action_space, dict_variables): + def _fill_dict_act_space(self, dict_, dict_variables): # TODO what about dict_variables !!! for attr_nm, sh, dt in zip( - action_space.attr_list_vect, action_space.shape, action_space.dtype + type(self._template_act).attr_list_vect, + self._template_act.shapes(), + self._template_act.dtypes() ): if sh == 0: # do not add "empty" (=0 dimension) arrays to gym otherwise it crashes @@ -249,7 +262,7 @@ def _fill_dict_act_space(self, dict_, action_space, dict_variables): my_type = type(self)._BoxType(low=-1, high=1, shape=shape, dtype=dt) elif attr_nm == "_set_topo_vect": my_type = type(self)._BoxType(low=-1, - high=type(action_space).n_busbar_per_sub, + high=type(self._template_act).n_busbar_per_sub, shape=shape, dtype=dt) elif dt == dt_bool: # boolean observation space @@ -263,28 +276,28 @@ def _fill_dict_act_space(self, dict_, action_space, dict_variables): SpaceType = type(self)._BoxType if attr_nm == "prod_p": - low = action_space.gen_pmin - high = action_space.gen_pmax + low = type(self._template_act).gen_pmin + high = type(self._template_act).gen_pmax shape = None elif attr_nm == "prod_v": # voltages can't be negative low = 0.0 elif attr_nm == "_redispatch": # redispatch - low = -1.0 * action_space.gen_max_ramp_down - high = 1.0 * action_space.gen_max_ramp_up - low[~action_space.gen_redispatchable] = 0.0 - high[~action_space.gen_redispatchable] = 0.0 + low = -1.0 * type(self._template_act).gen_max_ramp_down + high = 1.0 * type(self._template_act).gen_max_ramp_up + low[~type(self._template_act).gen_redispatchable] = 0.0 + high[~type(self._template_act).gen_redispatchable] = 0.0 elif attr_nm == "_curtail": # curtailment - low = np.zeros(action_space.n_gen, dtype=dt_float) - high = np.ones(action_space.n_gen, dtype=dt_float) - low[~action_space.gen_renewable] = -1.0 - high[~action_space.gen_renewable] = -1.0 + low = np.zeros(type(self._template_act).n_gen, dtype=dt_float) + high = np.ones(type(self._template_act).n_gen, dtype=dt_float) + low[~type(self._template_act).gen_renewable] = -1.0 + high[~type(self._template_act).gen_renewable] = -1.0 elif attr_nm == "_storage_power": # storage power - low = -1.0 * action_space.storage_max_p_prod - high = 1.0 * action_space.storage_max_p_absorb + low = -1.0 * type(self._template_act).storage_max_p_prod + high = 1.0 * type(self._template_act).storage_max_p_absorb my_type = SpaceType(low=low, high=high, shape=shape, dtype=dt) if my_type is None: @@ -317,10 +330,10 @@ def from_gym(self, gymlike_action: OrderedDict) -> object: if self.__is_converter: # case where the action space comes from a converter, in this case the converter takes the # delegation to convert the action to openai gym - res = self.initial_act_space.convert_action_from_gym(gymlike_action) + res = self._converter.convert_action_from_gym(gymlike_action) else: # case where the action space is a "simple" action space - res = self.initial_act_space() + res = self._template_act.copy() for k, v in gymlike_action.items(): internal_k = self.keys_human_2_grid2op[k] if internal_k in self._keys_encoding: @@ -347,7 +360,7 @@ def to_gym(self, action: object) -> OrderedDict: """ if self.__is_converter: - gym_action = self.initial_act_space.convert_action_to_gym(action) + gym_action = self._converter.convert_action_to_gym(action) else: # in that case action should be an instance of grid2op BaseAction assert isinstance( diff --git a/grid2op/gym_compat/gym_obs_space.py b/grid2op/gym_compat/gym_obs_space.py index f74b3e43a..170435d05 100644 --- a/grid2op/gym_compat/gym_obs_space.py +++ b/grid2op/gym_compat/gym_obs_space.py @@ -85,17 +85,46 @@ class __AuxGymObservationSpace: `env.gen_pmin` and `env.gen_pmax` are not always ensured in grid2op) """ - + ALLOWED_ENV_CLS = (Environment, MultiMixEnvironment, BaseMultiProcessEnvironment) def __init__(self, env, dict_variables=None): if not isinstance( - env, (Environment, MultiMixEnvironment, BaseMultiProcessEnvironment) + env, type(self).ALLOWED_ENV_CLS + (type(self), ) ): raise RuntimeError( "GymActionSpace must be created with an Environment of an ActionSpace (or a Converter)" ) - self._init_env = env - self.initial_obs_space = self._init_env.observation_space + # self._init_env = env + if isinstance(env, type(self).ALLOWED_ENV_CLS): + init_env_cls = type(env) + if init_env_cls._CLS_DICT_EXTENDED is None: + # make sure the _CLS_DICT_EXTENDED exists + tmp_ = {} + init_env_cls._make_cls_dict_extended(init_env_cls, res=tmp_, as_list=False, copy_=False, _topo_vect_only=False) + self.init_env_cls_dict = init_env_cls._CLS_DICT_EXTENDED.copy() + # retrieve an empty observation an disable the forecast feature + self.initial_obs = env.observation_space.get_empty_observation() + self.initial_obs._obs_env = None + self.initial_obs._ptr_kwargs_env = None + + if env.observation_space.obs_env is not None: + self._tol_poly = env.observation_space.obs_env._tol_poly + else: + self._tol_poly = 1e-2 + self._env_params = env.parameters + self._opp_attack_max_duration = env._oppSpace.attack_max_duration + elif isinstance(env, type(self)): + self.init_env_cls_dict = env.init_env_cls_dict.copy() + + # retrieve an empty observation an disable the forecast feature + self.initial_obs = env.initial_obs + + self._tol_poly = env._tol_poly + self._env_params = env._env_params + self._opp_attack_max_duration = env._opp_attack_max_duration + else: + raise RuntimeError("Unknown way to build a gym observation space") + dict_ = {} # will represent the gym.Dict space if dict_variables is None: @@ -105,48 +134,48 @@ def __init__(self, env, dict_variables=None): type(self)._BoxType( low=0., high=np.inf, - shape=(self._init_env.n_line, ), + shape=(self.init_env_cls_dict["n_line"], ), dtype=dt_float, ), "theta_or": type(self)._BoxType( low=-180., high=180., - shape=(self._init_env.n_line, ), + shape=(self.init_env_cls_dict["n_line"], ), dtype=dt_float, ), "theta_ex": type(self)._BoxType( low=-180., high=180., - shape=(self._init_env.n_line, ), + shape=(self.init_env_cls_dict["n_line"], ), dtype=dt_float, ), "load_theta": type(self)._BoxType( low=-180., high=180., - shape=(self._init_env.n_load, ), + shape=(self.init_env_cls_dict["n_load"], ), dtype=dt_float, ), "gen_theta": type(self)._BoxType( low=-180., high=180., - shape=(self._init_env.n_gen, ), + shape=(self.init_env_cls_dict["n_gen"], ), dtype=dt_float, ) } - if self._init_env.n_storage: + if self.init_env_cls_dict["n_storage"]: dict_variables["storage_theta"] = type(self)._BoxType( low=-180., high=180., - shape=(self._init_env.n_storage, ), + shape=(self.init_env_cls_dict["n_storage"], ), dtype=dt_float, ) self._fill_dict_obs_space( - dict_, env.observation_space, env.parameters, env._oppSpace, dict_variables + dict_, dict_variables ) super().__init__(dict_, dict_variables=dict_variables) # super should point to _BaseGymSpaceConverter @@ -202,11 +231,11 @@ def reencode_space(self, key, fun): f"Impossible to find key {key} in your observation space" ) my_dict[key] = fun - res = type(self)(self._init_env, my_dict) + res = type(self)(self, my_dict) return res def _fill_dict_obs_space( - self, dict_, observation_space, env_params, opponent_space, dict_variables={} + self, dict_, dict_variables={} ): for attr_nm in dict_variables: # case where the user specified a dedicated encoding @@ -214,17 +243,17 @@ def _fill_dict_obs_space( # none is by default to disable this feature continue if isinstance(dict_variables[attr_nm], type(self)._SpaceType): - if hasattr(observation_space._template_obj, attr_nm): + if hasattr(self.initial_obs, attr_nm): # add it only if attribute exists in the observation dict_[attr_nm] = dict_variables[attr_nm] else: dict_[attr_nm] = dict_variables[attr_nm].my_space - + # by default consider all attributes that are vectorized for attr_nm, sh, dt in zip( - observation_space.attr_list_vect, - observation_space.shape, - observation_space.dtype, + type(self.initial_obs).attr_list_vect, + self.initial_obs.shapes(), + self.initial_obs.dtypes(), ): if sh == 0: # do not add "empty" (=0 dimension) arrays to gym otherwise it crashes @@ -253,15 +282,15 @@ def _fill_dict_obs_space( my_type = type(self)._DiscreteType(n=8) elif attr_nm == "topo_vect": my_type = type(self)._BoxType(low=-1, - high=observation_space.n_busbar_per_sub, + high=self.init_env_cls_dict["n_busbar_per_sub"], shape=shape, dtype=dt) elif attr_nm == "time_before_cooldown_line": my_type = type(self)._BoxType( low=0, high=max( - env_params.NB_TIMESTEP_COOLDOWN_LINE, - env_params.NB_TIMESTEP_RECONNECTION, - opponent_space.attack_max_duration, + self._env_params.NB_TIMESTEP_COOLDOWN_LINE, + self._env_params.NB_TIMESTEP_RECONNECTION, + self._opp_attack_max_duration, ), shape=shape, dtype=dt, @@ -269,7 +298,7 @@ def _fill_dict_obs_space( elif attr_nm == "time_before_cooldown_sub": my_type = type(self)._BoxType( low=0, - high=env_params.NB_TIMESTEP_COOLDOWN_SUB, + high=self._env_params.NB_TIMESTEP_COOLDOWN_SUB, shape=shape, dtype=dt, ) @@ -314,17 +343,17 @@ def _fill_dict_obs_space( shape = (sh,) SpaceType = type(self)._BoxType if attr_nm == "gen_p" or attr_nm == "gen_p_before_curtail": - low = copy.deepcopy(observation_space.gen_pmin) - high = copy.deepcopy(observation_space.gen_pmax) + low = copy.deepcopy(self.init_env_cls_dict["gen_pmin"]) + high = copy.deepcopy(self.init_env_cls_dict["gen_pmax"]) shape = None # for redispatching - low -= observation_space.obs_env._tol_poly - high += observation_space.obs_env._tol_poly + low -= self._tol_poly + high += self._tol_poly # for "power losses" that are not properly computed in the original data extra_for_losses = _compute_extra_power_for_losses( - observation_space + self.init_env_cls_dict ) low -= extra_for_losses high += extra_for_losses @@ -343,17 +372,17 @@ def _fill_dict_obs_space( elif attr_nm == "target_dispatch" or attr_nm == "actual_dispatch": # TODO check that to be sure low = np.minimum( - observation_space.gen_pmin, -observation_space.gen_pmax + self.init_env_cls_dict["gen_pmin"], -self.init_env_cls_dict["gen_pmax"] ) high = np.maximum( - -observation_space.gen_pmin, +observation_space.gen_pmax + -self.init_env_cls_dict["gen_pmin"], +self.init_env_cls_dict["gen_pmax"] ) elif attr_nm == "storage_power" or attr_nm == "storage_power_target": - low = -observation_space.storage_max_p_prod - high = observation_space.storage_max_p_absorb + low = -self.init_env_cls_dict["storage_max_p_prod"] + high = self.init_env_cls_dict["storage_max_p_absorb"] elif attr_nm == "storage_charge": - low = np.zeros(observation_space.n_storage, dtype=dt_float) - high = observation_space.storage_Emax + low = np.zeros(self.init_env_cls_dict["n_storage"], dtype=dt_float) + high = self.init_env_cls_dict["storage_Emax"] elif ( attr_nm == "curtailment" or attr_nm == "curtailment_limit" @@ -369,10 +398,10 @@ def _fill_dict_obs_space( high = np.inf elif attr_nm == "gen_margin_up": low = 0.0 - high = observation_space.gen_max_ramp_up + high = self.init_env_cls_dict["gen_max_ramp_up"] elif attr_nm == "gen_margin_down": low = 0.0 - high = observation_space.gen_max_ramp_down + high = self.init_env_cls_dict["gen_max_ramp_down"] # curtailment, curtailment_limit, gen_p_before_curtail my_type = SpaceType(low=low, high=high, shape=shape, dtype=dt) @@ -396,7 +425,7 @@ def from_gym(self, gymlike_observation: spaces.dict.OrderedDict) -> BaseObservat grid2oplike_observation: :class:`grid2op.Observation.BaseObservation` The corresponding grid2op observation """ - res = self.initial_obs_space.get_empty_observation() + res = self.initial_obs.copy() for k, v in gymlike_observation.items(): try: res._assign_attr_from_name(k, v) diff --git a/grid2op/gym_compat/gymenv.py b/grid2op/gym_compat/gymenv.py index 15446e6b8..0584ff2ae 100644 --- a/grid2op/gym_compat/gymenv.py +++ b/grid2op/gym_compat/gymenv.py @@ -107,16 +107,28 @@ class behave differently depending on the version of gym you have installed ! def __init__(self, env_init: Environment, shuffle_chronics:Optional[bool]=True, - render_mode: Literal["rgb_array"]="rgb_array"): + render_mode: Literal["rgb_array"]="rgb_array", + with_forecast: bool=False): cls = type(self) check_gym_version(cls._gymnasium) + self.action_space = cls._ActionSpaceType(env_init) + self.observation_space = cls._ObservationSpaceType(env_init) + self.reward_range = env_init.reward_range + self.metadata = env_init.metadata self.init_env = env_init.copy() - self.action_space = cls._ActionSpaceType(self.init_env) - self.observation_space = cls._ObservationSpaceType(self.init_env) - self.reward_range = self.init_env.reward_range - self.metadata = self.init_env.metadata self.init_env.render_mode = render_mode self._shuffle_chronics = shuffle_chronics + if not with_forecast: + # default in grid2op 1.10.3 + # to improve pickle compatibility and speed + self.init_env.deactivate_forecast() + self.init_env._observation_space.obs_env.close() + self.init_env._observation_space.obs_env = None + self.init_env._observation_space._ObsEnv_class = None + self.init_env._last_obs._obs_env = None + self.init_env._last_obs._ptr_kwargs_env = False + self.init_env.current_obs._obs_env = None + self.init_env.current_obs._ptr_kwargs_env = False super().__init__() # super should reference either gym.Env or gymnasium.Env if not hasattr(self, "_np_random"): @@ -219,11 +231,11 @@ def _aux_seed_spaces(self): self.observation_space.seed(next_seed) def _aux_seed_g2op(self, seed): - # then seed the underlying grid2op env - max_ = np.iinfo(dt_int).max - next_seed = sample_seed(max_, self._np_random) - underlying_env_seeds = self.init_env.seed(next_seed) - return seed, next_seed, underlying_env_seeds + # then seed the underlying grid2op env + max_ = np.iinfo(dt_int).max + next_seed = sample_seed(max_, self._np_random) + underlying_env_seeds = self.init_env.seed(next_seed) + return seed, next_seed, underlying_env_seeds def _aux_seed(self, seed: Optional[int]=None): # deprecated in gym >=0.26 diff --git a/grid2op/gym_compat/utils.py b/grid2op/gym_compat/utils.py index 030fa89bb..4374ae4a1 100644 --- a/grid2op/gym_compat/utils.py +++ b/grid2op/gym_compat/utils.py @@ -104,7 +104,8 @@ def _compute_extra_power_for_losses(gridobj): to handle the "because of the power losses gen_pmin and gen_pmax can be slightly altered" """ import numpy as np - + if isinstance(gridobj, dict): + return 0.3*np.abs(gridobj["gen_pmax"]).sum() return 0.3 * np.abs(gridobj.gen_pmax).sum() diff --git a/grid2op/tests/BaseBackendTest.py b/grid2op/tests/BaseBackendTest.py index 10c8b7e87..b75f32e35 100644 --- a/grid2op/tests/BaseBackendTest.py +++ b/grid2op/tests/BaseBackendTest.py @@ -2190,11 +2190,13 @@ def tearDown(self): def test_reset_equals_reset(self): self.skip_if_needed() - # Reset backend1 with reset - self.env1.reset() - # Reset backend2 with reset - self.env2.reset() - self._compare_backends() + with warnings.catch_warnings(): + warnings.filterwarnings("error") + # Reset backend1 with reset + self.env1.reset() + # Reset backend2 with reset + self.env2.reset() + self._compare_backends() def _compare_backends(self): # Compare diff --git a/grid2op/tests/_aux_test_some_gym_issues.py b/grid2op/tests/_aux_test_some_gym_issues.py index 5534865f4..c1c065da3 100644 --- a/grid2op/tests/_aux_test_some_gym_issues.py +++ b/grid2op/tests/_aux_test_some_gym_issues.py @@ -19,7 +19,7 @@ from test_issue_379 import Issue379Tester from test_issue_407 import Issue407Tester from test_issue_418 import Issue418Tester -from test_gym_compat import (TestGymCompatModule, +from test_defaultgym_compat import (TestGymCompatModule, TestBoxGymObsSpace, TestBoxGymActSpace, TestMultiDiscreteGymActSpace, @@ -38,6 +38,15 @@ ) from test_timeOutEnvironment import TestTOEnvGym from test_pickling import TestMultiProc +from test_alert_gym_compat import * +from test_basic_env_ls import TestBasicEnvironmentGym +from test_gym_asynch_env import * +from test_l2rpn_idf_2023 import TestL2RPNIDF2023Tester +from test_MaskedEnvironment import TestMaskedEnvironmentGym +from test_multidiscrete_act_space import * +from test_n_busbar_per_sub import TestGym_3busbars, TestGym_1busbar +from test_timeOutEnvironment import TestTOEnvGym + if __name__ == "__main__": unittest.main() diff --git a/grid2op/tests/automatic_classes.py b/grid2op/tests/automatic_classes.py new file mode 100644 index 000000000..57306c486 --- /dev/null +++ b/grid2op/tests/automatic_classes.py @@ -0,0 +1,799 @@ +# Copyright (c) 2024, 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. + +import os +import multiprocessing as mp +from typing import Optional +import warnings +import unittest +import importlib +import numpy as np +from gymnasium.vector import AsyncVectorEnv + + +import grid2op +from grid2op._glop_platform_info import _IS_WINDOWS +from grid2op.Runner import Runner +from grid2op.Agent import BaseAgent +from grid2op.Action import BaseAction +from grid2op.Observation.baseObservation import BaseObservation +from grid2op.Action.actionSpace import ActionSpace +from grid2op.Environment import (Environment, + MaskedEnvironment, + TimedOutEnvironment, + SingleEnvMultiProcess, + MultiMixEnvironment) +from grid2op.Exceptions import NoForecastAvailable +from grid2op.gym_compat import (GymEnv, + BoxGymActSpace, + BoxGymObsSpace, + DiscreteActSpace, + MultiDiscreteActSpace) + +# TODO test the runner saved classes and reload + +# TODO two envs same name => now diff classes + +# TODO test add_to_name +# TODO test noshunt +# TODO grid2op compat version + +# TODO test backend converters +# TODO test all type of backend in the observation space, including the deactivate forecast, reactivate forecast, the different backend etc. + +class _ThisAgentTest(BaseAgent): + def __init__(self, + action_space: ActionSpace, + _read_from_local_dir, + _name_cls_obs, + _name_cls_act, + ): + super().__init__(action_space) + self._read_from_local_dir = _read_from_local_dir + self._name_cls_obs = _name_cls_obs + self._name_cls_act = _name_cls_act + + def act(self, observation: BaseObservation, reward: float, done: bool = False) -> BaseAction: + supermodule_nm, module_nm = os.path.split(self._read_from_local_dir) + super_module = importlib.import_module(module_nm, supermodule_nm) + + # check observation + this_module = importlib.import_module(f"{module_nm}.{self._name_cls_obs}_file", super_module) + if hasattr(this_module, self._name_cls_obs): + this_class_obs = getattr(this_module, self._name_cls_obs) + else: + raise RuntimeError(f"class {self._name_cls_obs} not found") + assert isinstance(observation, this_class_obs) + + # check action + this_module = importlib.import_module(f"{module_nm}.{self._name_cls_act}_file", super_module) + if hasattr(this_module, self._name_cls_act): + this_class_act = getattr(this_module, self._name_cls_act) + else: + raise RuntimeError(f"class {self._name_cls_act} not found") + res = super().act(observation, reward, done) + assert isinstance(res, this_class_act) + return res + + +class AutoClassMakeTester(unittest.TestCase): + """test that the kwargs `class_in_file=False` erase the default behaviour """ + def test_in_make(self): + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + env = grid2op.make("l2rpn_case14_sandbox", test=True, class_in_file=False) + assert env._read_from_local_dir is None + assert not env.classes_are_in_files() + + +class AutoClassInFileTester(unittest.TestCase): + def get_env_name(self): + return "l2rpn_case14_sandbox" + + def setUp(self) -> None: + self.max_iter = 10 + return super().setUp() + + def _do_test_runner(self): + # false for multi process env + return True + + def _do_test_copy(self): + # for for multi process env + return True + + def _do_test_obs_env(self): + return True + + def _aux_make_env(self, env: Optional[Environment]=None): + if env is None: + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + env = grid2op.make(self.get_env_name(), test=True, class_in_file=True) + assert env.classes_are_in_files() + return env + + def _aux_get_obs_cls(self): + return "CompleteObservation_{}" + + def _aux_get_act_cls(self): + return "PlayableAction_{}" + + def test_all_classes_from_file(self, + env: Optional[Environment]=None, + classes_name=None, + name_complete_obs_cls="CompleteObservation_{}", + name_observation_cls=None, + name_action_cls=None): + if classes_name is None: + classes_name = self.get_env_name() + if name_observation_cls is None: + name_observation_cls = self._aux_get_obs_cls().format(classes_name) + if name_action_cls is None: + name_action_cls = self._aux_get_act_cls().format(classes_name) + + name_action_cls = name_action_cls.format(classes_name) + env = self._aux_make_env(env) + names_cls = [f"ActionSpace_{classes_name}", + f"_BackendAction_{classes_name}", + f"CompleteAction_{classes_name}", + name_observation_cls.format(classes_name), + name_complete_obs_cls.format(classes_name), + f"DontAct_{classes_name}", + f"_ObsEnv_{classes_name}", + f"ObservationSpace_{classes_name}", + f"PandaPowerBackend_{classes_name}", + name_action_cls, + f"VoltageOnlyAction_{classes_name}" + ] + names_attr = ["action_space", + "_backend_action_class", + "_complete_action_cls", + "_observationClass", + None, # Complete Observation in the forecast ! + None, # DONT ACT not int ENV directly + None, # ObsEnv NOT IN ENV, + "observation_space", + "backend", + "_actionClass", + None, # VoltageOnlyAction not in env + ] + + # NB: these imports needs to be consistent with what is done in + # base_env.generate_classes() and gridobj.init_grid(...) + supermodule_nm, module_nm = os.path.split(env._read_from_local_dir) + super_module = importlib.import_module(module_nm, supermodule_nm) + for name_cls, name_attr in zip(names_cls, names_attr): + this_module = importlib.import_module(f"{module_nm}.{name_cls}_file", super_module) + if hasattr(this_module, name_cls): + this_class = getattr(this_module, name_cls) + else: + raise RuntimeError(f"class {name_cls} not found") + if name_attr is not None: + the_attr = getattr(env, name_attr) + if isinstance(the_attr, type): + assert the_attr is this_class, f"error for {the_attr} vs {this_class} env.{name_attr}" + else: + assert type(the_attr) is this_class, f"error for {type(the_attr)} vs {this_class} (env.{name_attr})" + assert this_class._CLS_DICT is not None, f'error for {name_cls}' + assert this_class._CLS_DICT_EXTENDED is not None, f'error for {name_cls}' + + # additional check for some attributes + if name_cls == f"ActionSpace_{classes_name}": + assert type(env._helper_action_env) is this_class + if env.observation_space.obs_env is not None: + # not in _ObsEnv + assert type(env.observation_space.obs_env._helper_action_env) is this_class, f"{type(env.observation_space.obs_env._helper_action_env)}" + if env._voltage_controler is not None: + # not in _ObsEnv + assert type(env._voltage_controler.action_space) is this_class + if env.chronics_handler.action_space is not None: + # not in _ObsEnv + assert type(env.chronics_handler.action_space) is this_class + assert env.chronics_handler.action_space is env._helper_action_env + elif name_cls == f"_BackendAction_{classes_name}": + assert env.backend.my_bk_act_class is this_class + assert isinstance(env._backend_action, this_class) + if env.observation_space.obs_env is not None: + # not in _ObsEnv + assert env.observation_space.obs_env._backend_action_class is this_class + assert env.observation_space.obs_env.backend.my_bk_act_class is this_class + assert isinstance(env.observation_space.obs_env._backend_action, this_class) + elif name_cls == f"CompleteAction_{classes_name}": + assert env.backend._complete_action_class is this_class + + if env.observation_space.obs_env is not None: + # not in _ObsEnv + assert env.observation_space.obs_env._complete_action_cls is this_class + assert env.observation_space.obs_env.backend._complete_action_class is this_class + + assert env.observation_space.obs_env._actionClass is this_class + + assert env._helper_action_env.subtype is this_class + elif name_cls == name_observation_cls.format(classes_name): + # observation of the env + assert env._observation_space.subtype is this_class + if env.current_obs is not None: + # not in _ObsEnv + assert isinstance(env.current_obs, this_class) + if env._last_obs is not None: + # not in _ObsEnv + assert isinstance(env._last_obs, this_class) + elif name_cls == name_observation_cls.format(classes_name): + # observation of the forecast + if env.observation_space.obs_env is not None: + # not in _ObsEnv + assert env._observation_space.obs_env._observation_space.subtype is this_class + if env.observation_space.obs_env.current_obs is not None: + # not in _ObsEnv + assert isinstance(env.observation_space.obs_env.current_obs, this_class) + if env.observation_space.obs_env._last_obs is not None: + # not in _ObsEnv + assert isinstance(env.observation_space.obs_env._last_obs, this_class) + elif name_cls == f"DontAct_{classes_name}": + assert env._oppSpace.action_space.subtype is this_class + assert env._opponent.action_space.subtype is this_class + elif name_cls == f"_ObsEnv_{classes_name}": + if env.observation_space.obs_env is not None: + # not in _ObsEnv + assert type(env.observation_space.obs_env) is this_class + assert isinstance(env.observation_space.obs_env, this_class) + if env.current_obs is not None and env.current_obs._obs_env is not None: + # not in _ObsEnv + assert type(env.current_obs._obs_env) is this_class, f"{type(env.current_obs._obs_env)}" + assert isinstance(env.observation_space.obs_env, this_class) + if env._last_obs is not None and env._last_obs._obs_env is not None: + # not in _ObsEnv + assert type(env._last_obs._obs_env) is this_class, f"{type(env._last_obs._obs_env)}" + if env.observation_space.obs_env is not None: + # not in _ObsEnv + assert env.current_obs._obs_env is env.observation_space.obs_env + assert env._last_obs._obs_env is env.observation_space.obs_env + elif name_cls == f"ObservationSpace_{classes_name}": + if env.observation_space.obs_env is not None: + # not in _ObsEnv + assert type(env.observation_space.obs_env._observation_space) is this_class + assert type(env.observation_space.obs_env._ptr_orig_obs_space) is this_class, f"{type(env.observation_space.obs_env._ptr_orig_obs_space)}" + + assert env.observation_space.obs_env._ptr_orig_obs_space is env._observation_space, f"{type(env.observation_space.obs_env._ptr_orig_obs_space)}" + elif name_cls == name_action_cls: + assert env._action_space.subtype is this_class + # assert env.observation_space.obs_env._actionClass is this_class # not it's a complete action apparently + elif name_cls == f"VoltageOnlyAction_{classes_name}": + if env._voltage_controler is not None: + # not in _ObsEnv + assert env._voltage_controler.action_space.subtype is this_class + # TODO test current_obs and _last_obs + + def test_all_classes_from_file_env_after_reset(self, env: Optional[Environment]=None): + """test classes are still consistent even after a call to env.reset() and obs.simulate()""" + env = self._aux_make_env(env) + obs = env.reset() + self.test_all_classes_from_file(env=env) + try: + obs.simulate(env.action_space()) + self.test_all_classes_from_file(env=env) + except NoForecastAvailable: + # cannot do this test if the "original" env is a _Forecast env: + # for l2rpn_case14_sandbox only 1 step ahead forecast are available + pass + + def test_all_classes_from_file_obsenv(self, env: Optional[Environment]=None): + """test the files are correctly generated for the "forecast env" in the + environment even after a call to obs.reset() and obs.simulate()""" + if not self._do_test_obs_env(): + self.skipTest("ObsEnv is not tested") + env = self._aux_make_env(env) + + self.test_all_classes_from_file(env=env.observation_space.obs_env, + name_action_cls="CompleteAction_{}", + name_observation_cls="CompleteObservation_{}") + + # reset and check the same + obs = env.reset() + self.test_all_classes_from_file(env=env.observation_space.obs_env, + name_action_cls="CompleteAction_{}", + name_observation_cls="CompleteObservation_{}") + self.test_all_classes_from_file(env=obs._obs_env, + name_action_cls="CompleteAction_{}", + name_observation_cls="CompleteObservation_{}") + + # forecast and check the same + try: + obs.simulate(env.action_space()) + self.test_all_classes_from_file(env=env.observation_space.obs_env, + name_action_cls="CompleteAction_{}", + name_observation_cls="CompleteObservation_{}") + self.test_all_classes_from_file(env=obs._obs_env, + name_action_cls="CompleteAction_{}", + name_observation_cls="CompleteObservation_{}") + except NoForecastAvailable: + # cannot do this test if the "original" env is a _Forecast env: + # for l2rpn_case14_sandbox only 1 step ahead forecast are available + pass + + def test_all_classes_from_file_env_cpy(self, env: Optional[Environment]=None): + """test that when an environment is copied, then the copied env is consistent, + that it is consistent after a reset and that the forecast env is consistent""" + if not self._do_test_copy(): + self.skipTest("Copy is not tested") + env = self._aux_make_env(env) + env_cpy = env.copy() + self.test_all_classes_from_file(env=env_cpy) + self.test_all_classes_from_file_env_after_reset(env=env_cpy) + self.test_all_classes_from_file(env=env_cpy.observation_space.obs_env, + name_action_cls="CompleteAction_{}", + name_observation_cls="CompleteObservation_{}" + ) + self.test_all_classes_from_file_obsenv(env=env_cpy) + + def test_all_classes_from_file_env_runner(self, env: Optional[Environment]=None): + """this test, using the defined functions above that the runner is able to create a valid env""" + if not self._do_test_runner(): + self.skipTest("Runner not tested") + env = self._aux_make_env(env) + runner = Runner(**env.get_params_for_runner()) + env_runner = runner.init_env() + self.test_all_classes_from_file(env=env_runner) + self.test_all_classes_from_file_env_after_reset(env=env_runner) + self.test_all_classes_from_file(env=env_runner.observation_space.obs_env, + name_action_cls="CompleteAction_{}", + name_observation_cls="CompleteObservation_{}") + self.test_all_classes_from_file_obsenv(env=env_runner) + + # test the runner prevents the deletion of the tmp file where the classes are stored + # path_cls = env._local_dir_cls + # del env + # assert os.path.exists(path_cls.name) + env_runner = runner.init_env() + self.test_all_classes_from_file(env=env_runner) + self.test_all_classes_from_file_env_after_reset(env=env_runner) + self.test_all_classes_from_file(env=env_runner.observation_space.obs_env, + name_action_cls="CompleteAction_{}", + name_observation_cls="CompleteObservation_{}") + self.test_all_classes_from_file_obsenv(env=env_runner) + + def test_all_classes_from_file_runner_1ep(self, env: Optional[Environment]=None): + """this test that the runner is able to "run" (one type of run), but the tests on the classes + are much lighter than in test_all_classes_from_file_env_runner""" + if not self._do_test_runner(): + self.skipTest("Runner not tested") + env = self._aux_make_env(env) + this_agent = _ThisAgentTest(env.action_space, + env._read_from_local_dir, + self._aux_get_obs_cls().format(self.get_env_name()), + self._aux_get_act_cls().format(self.get_env_name()), + ) + runner = Runner(**env.get_params_for_runner(), + agentClass=None, + agentInstance=this_agent) + runner.run(nb_episode=1, + max_iter=self.max_iter, + env_seeds=[0], + episode_id=[0]) + + def test_all_classes_from_file_runner_2ep_seq(self, env: Optional[Environment]=None): + """this test that the runner is able to "run" (one other type of run), but the tests on the classes + are much lighter than in test_all_classes_from_file_env_runner""" + if not self._do_test_runner(): + self.skipTest("Runner not tested") + env = self._aux_make_env(env) + this_agent = _ThisAgentTest(env.action_space, + env._read_from_local_dir, + self._aux_get_obs_cls().format(self.get_env_name()), + self._aux_get_act_cls().format(self.get_env_name()), + ) + runner = Runner(**env.get_params_for_runner(), + agentClass=None, + agentInstance=this_agent) + res = runner.run(nb_episode=2, + max_iter=self.max_iter, + env_seeds=[0, 0], + episode_id=[0, 1]) + assert res[0][4] == self.max_iter + assert res[1][4] == self.max_iter + + def test_all_classes_from_file_runner_2ep_par_fork(self, env: Optional[Environment]=None): + """this test that the runner is able to "run" (one other type of run), but the tests on the classes + are much lighter than in test_all_classes_from_file_env_runner""" + if not self._do_test_runner(): + self.skipTest("Runner not tested") + if _IS_WINDOWS: + self.skipTest("no fork on windows") + env = self._aux_make_env(env) + this_agent = _ThisAgentTest(env.action_space, + env._read_from_local_dir, + self._aux_get_obs_cls().format(self.get_env_name()), + self._aux_get_act_cls().format(self.get_env_name()), + ) + ctx = mp.get_context('fork') + runner = Runner(**env.get_params_for_runner(), + agentClass=None, + agentInstance=this_agent, + mp_context=ctx) + res = runner.run(nb_episode=2, + nb_process=2, + max_iter=self.max_iter, + env_seeds=[0, 0], + episode_id=[0, 1]) + assert res[0][4] == self.max_iter + assert res[1][4] == self.max_iter + + def test_all_classes_from_file_runner_2ep_par_spawn(self, env: Optional[Environment]=None): + """this test that the runner is able to "run" (one other type of run), but the tests on the classes + are much lighter than in test_all_classes_from_file_env_runner""" + if not self._do_test_runner(): + self.skipTest("Runner not tested") + env = self._aux_make_env(env) + this_agent = _ThisAgentTest(env.action_space, + env._read_from_local_dir, + self._aux_get_obs_cls().format(self.get_env_name()), + self._aux_get_act_cls().format(self.get_env_name()), + ) + ctx = mp.get_context('spawn') + runner = Runner(**env.get_params_for_runner(), + agentClass=None, + agentInstance=this_agent, + mp_context=ctx) + res = runner.run(nb_episode=2, + nb_process=2, + max_iter=self.max_iter, + env_seeds=[0, 0], + episode_id=[0, 1]) + assert res[0][4] == self.max_iter + assert res[1][4] == self.max_iter + + +class MaskedEnvAutoClassTester(AutoClassInFileTester): + + def _aux_make_env(self, env: Optional[Environment]=None): + if env is None: + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + env = MaskedEnvironment(super()._aux_make_env(), + lines_of_interest=np.array([True, True, True, True, True, True, + False, False, False, False, False, False, + False, False, False, False, False, False, + False, False])) + return env + + +class TOEnvAutoClassTester(AutoClassInFileTester): + + def _aux_make_env(self, env: Optional[Environment]=None): + if env is None: + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + env = TimedOutEnvironment(super()._aux_make_env(), + time_out_ms=1e-3) + return env + + +class ForEnvAutoClassTester(AutoClassInFileTester): + + def _aux_make_env(self, env: Optional[Environment]=None): + if env is None: + # we create the reference environment and prevent grid2op to + # to delete it (because it stores the files to the class) + self.ref_env = super()._aux_make_env() + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + obs = self.ref_env.get_obs() + res = obs.get_forecast_env() + self.max_iter = res._max_iter # otherwise it fails in the runner + else: + res = env + return res + + def tearDown(self): + if hasattr(self, "ref_env"): + self.ref_env.close() + + +# class SEMPAUtoClassTester(AutoClassInFileTester): +# """means i need to completely recode `test_all_classes_from_file` to take into account the return +# values which is a list now... and i'm not ready for it yet TODO""" +# def _do_test_runner(self): +# # false for multi process env +# return False + +# def _do_test_copy(self): +# # for for multi process env +# return False + +# def _do_test_obs_env(self): +# return False + +# def _aux_make_env(self, env: Optional[Environment]=None): +# if env is None: +# # we create the reference environment and prevent grid2op to +# # to delete it (because it stores the files to the class) +# self.ref_env = super()._aux_make_env() +# with warnings.catch_warnings(): +# warnings.filterwarnings("ignore") +# res = SingleEnvMultiProcess(self.ref_env, nb_env=2) +# else: +# res = env +# return res + +class GymEnvAutoClassTester(unittest.TestCase): + def setUp(self) -> None: + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + self.env = grid2op.make("l2rpn_case14_sandbox", + test=True, + class_in_file=True) + self.line_id = 3 + th_lim = self.env.get_thermal_limit() * 2. # avoid all problem in general + th_lim[self.line_id] /= 10. # make sure to get trouble in line 3 + self.env.set_thermal_limit(th_lim) + + GymEnvAutoClassTester._init_env(self.env) + + @staticmethod + def _init_env(env): + env.set_id(0) + env.seed(0) + env.reset() + + def tearDown(self) -> None: + self.env.close() + return super().tearDown() + + def _aux_run_envs(self, act, env_gym): + for i in range(10): + obs_in, reward, done, truncated, info = env_gym.step(act) + if i < 2: # 2 : 2 full steps already + assert obs_in["timestep_overflow"][self.line_id] == i + 1, f"error for step {i}: {obs_in['timestep_overflow'][self.line_id]}" + else: + # cooldown applied for line 3: + # - it disconnect stuff in `self.env_in` + # - it does not affect anything in `self.env_out` + assert not obs_in["line_status"][self.line_id] + + def test_gym_with_step(self): + """test the step function also disconnects (or not) the lines""" + env_gym = GymEnv(self.env) + act = {} + self._aux_run_envs(act, env_gym) + env_gym.reset() + self._aux_run_envs(act, env_gym) + + def test_gym_normal(self): + """test I can create the gym env""" + env_gym = GymEnv(self.env) + env_gym.reset() + + def test_gym_box(self): + """test I can create the gym env with box ob space and act space""" + env_gym = GymEnv(self.env) + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + env_gym.action_space = BoxGymActSpace(self.env.action_space) + env_gym.observation_space = BoxGymObsSpace(self.env.observation_space) + env_gym.reset() + + def test_gym_discrete(self): + """test I can create the gym env with discrete act space""" + env_gym = GymEnv(self.env) + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + env_gym.action_space = DiscreteActSpace(self.env.action_space) + env_gym.reset() + act = 0 + self._aux_run_envs(act, env_gym) + + def test_gym_multidiscrete(self): + """test I can create the gym env with multi discrete act space""" + env_gym = GymEnv(self.env) + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + env_gym.action_space = MultiDiscreteActSpace(self.env.action_space) + env_gym.reset() + act = env_gym.action_space.sample() + act[:] = 0 + self._aux_run_envs(act, env_gym) + + def test_asynch_fork(self): + if _IS_WINDOWS: + self.skipTest("no fork on windows") + async_vect_env = AsyncVectorEnv((lambda: GymEnv(self.env), lambda: GymEnv(self.env)), + context="fork") + obs = async_vect_env.reset() + + def test_asynch_spawn(self): + async_vect_env = AsyncVectorEnv((lambda: GymEnv(self.env), lambda: GymEnv(self.env)), + context="spawn") + obs = async_vect_env.reset() + + +class MultiMixEnvAutoClassTester(AutoClassInFileTester): + def _aux_get_obs_cls(self): + return "ObservationNeurips2020_{}" + + def _aux_get_act_cls(self): + return "ActionNeurips2020_{}" + + def get_env_name(self): + return "l2rpn_neurips_2020_track2" + # TODO gym for that too + + # def _do_test_runner(self): + # return False + + def test_all_classes_from_file(self, + env: Optional[Environment]=None, + classes_name=None, + name_complete_obs_cls="CompleteObservation_{}", + name_observation_cls=None, + name_action_cls=None): + env_orig = env + env = self._aux_make_env(env) + try: + super().test_all_classes_from_file(env, + classes_name=classes_name, + name_complete_obs_cls=name_complete_obs_cls, + name_observation_cls=name_observation_cls, + name_action_cls=name_action_cls + ) + if isinstance(env, MultiMixEnvironment): + # test each mix of a multi mix + for mix in env: + super().test_all_classes_from_file(mix, + classes_name=classes_name, + name_complete_obs_cls=name_complete_obs_cls, + name_observation_cls=name_observation_cls, + name_action_cls=name_action_cls + ) + finally: + if env_orig is None: + # need to clean the env I created + env.close() + + def test_all_classes_from_file_env_after_reset(self, env: Optional[Environment]=None): + env_orig = env + env = self._aux_make_env(env) + try: + super().test_all_classes_from_file_env_after_reset(env) + if isinstance(env, MultiMixEnvironment): + # test each mix of a multimix + for mix in env: + super().test_all_classes_from_file_env_after_reset(mix) + finally: + if env_orig is None: + # need to clean the env I created + env.close() + + def test_all_classes_from_file_obsenv(self, env: Optional[Environment]=None): + env_orig = env + env = self._aux_make_env(env) + try: + super().test_all_classes_from_file_obsenv(env) + if isinstance(env, MultiMixEnvironment): + # test each mix of a multimix + for mix in env: + super().test_all_classes_from_file_obsenv(mix) + finally: + if env_orig is None: + # need to clean the env I created + env.close() + + def test_all_classes_from_file_env_cpy(self, env: Optional[Environment]=None): + env_orig = env + env = self._aux_make_env(env) + try: + super().test_all_classes_from_file_env_cpy(env) + if isinstance(env, MultiMixEnvironment): + # test each mix of a multimix + for mix in env: + super().test_all_classes_from_file_env_cpy(mix) + finally: + if env_orig is None: + # need to clean the env I created + env.close() + + def test_all_classes_from_file_env_runner(self, env: Optional[Environment]=None): + env_orig = env + env = self._aux_make_env(env) + try: + if isinstance(env, MultiMixEnvironment): + # test each mix of a multimix + for mix in env: + super().test_all_classes_from_file_env_runner(mix) + else: + # runner does not handle multimix + super().test_all_classes_from_file_env_runner(env) + finally: + if env_orig is None: + # need to clean the env I created + env.close() + + def test_all_classes_from_file_runner_1ep(self, env: Optional[Environment]=None): + env_orig = env + env = self._aux_make_env(env) + try: + if isinstance(env, MultiMixEnvironment): + # test each mix of a multimix + for mix in env: + super().test_all_classes_from_file_runner_1ep(mix) + else: + # runner does not handle multimix + super().test_all_classes_from_file_runner_1ep(env) + finally: + if env_orig is None: + # need to clean the env I created + env.close() + + def test_all_classes_from_file_runner_2ep_seq(self, env: Optional[Environment]=None): + env_orig = env + env = self._aux_make_env(env) + try: + if isinstance(env, MultiMixEnvironment): + # test each mix of a multimix + for mix in env: + super().test_all_classes_from_file_runner_2ep_seq(mix) + else: + # runner does not handle multimix + super().test_all_classes_from_file_runner_2ep_seq(env) + finally: + if env_orig is None: + # need to clean the env I created + env.close() + + def test_all_classes_from_file_runner_2ep_par_fork(self, env: Optional[Environment]=None): + if _IS_WINDOWS: + self.skipTest("no fork on windows") + env_orig = env + env = self._aux_make_env(env) + try: + if isinstance(env, MultiMixEnvironment): + # test each mix of a multimix + for mix in env: + super().test_all_classes_from_file_runner_2ep_par_fork(mix) + else: + # runner does not handle multimix + super().test_all_classes_from_file_runner_2ep_par_fork(env) + finally: + if env_orig is None: + # need to clean the env I created + env.close() + + def test_all_classes_from_file_runner_2ep_par_spawn(self, env: Optional[Environment]=None): + env_orig = env + env = self._aux_make_env(env) + try: + if isinstance(env, MultiMixEnvironment): + # test each mix of a multimix + for mix in env: + super().test_all_classes_from_file_runner_2ep_par_spawn(mix) + else: + # runner does not handle multimix + super().test_all_classes_from_file_runner_2ep_par_spawn(env) + finally: + if env_orig is None: + # need to clean the env I created + env.close() + + def test_forecast_env_basic(self, env: Optional[Environment]=None): + env_orig = env + env = self._aux_make_env(env) + try: + if isinstance(env, MultiMixEnvironment): + # test each mix of a multimix + for mix in env: + obs = mix.reset() + for_env = obs.get_forecast_env() + super().test_all_classes_from_file(for_env) + finally: + if env_orig is None: + # need to clean the env I created + env.close() + + +if __name__ == "__main__": + unittest.main() diff --git a/grid2op/tests/test_fromChronix2grid.py b/grid2op/tests/fromChronix2grid.py similarity index 100% rename from grid2op/tests/test_fromChronix2grid.py rename to grid2op/tests/fromChronix2grid.py diff --git a/grid2op/tests/test_CompactEpisodeData.py b/grid2op/tests/test_CompactEpisodeData.py index 5fcdeeeae..11f9dec78 100644 --- a/grid2op/tests/test_CompactEpisodeData.py +++ b/grid2op/tests/test_CompactEpisodeData.py @@ -260,6 +260,40 @@ def test_with_opponent(self): lines_impacted, subs_impacted = episode_data.attack_space.from_vect(episode_data.attacks[0]).get_topological_impact() assert lines_impacted[3] + def test_can_return_ep_data(self): + # One episode + res = self.runner.run(nb_episode=1, + episode_id=[0], + env_seeds=[0], + max_iter=self.max_iter, + add_detailed_output=True, + nb_process=1 + ) + for el in res: + assert isinstance(el[-1], CompactEpisodeData) + + # 2 episodes, sequential mode + res = self.runner.run(nb_episode=2, + episode_id=[0, 1], + env_seeds=[0, 1], + max_iter=self.max_iter, + add_detailed_output=True, + nb_process=1 + ) + for el in res: + assert isinstance(el[-1], CompactEpisodeData) + + # 2 episodes, parrallel mode + res = self.runner.run(nb_episode=2, + episode_id=[0, 1], + env_seeds=[0, 1], + max_iter=self.max_iter, + add_detailed_output=True, + nb_process=2 + ) + for el in res: + assert isinstance(el[-1], CompactEpisodeData) + if __name__ == "__main__": unittest.main() diff --git a/grid2op/tests/test_MultiMix.py b/grid2op/tests/test_MultiMix.py index 9ee05802f..0f66ed0b0 100644 --- a/grid2op/tests/test_MultiMix.py +++ b/grid2op/tests/test_MultiMix.py @@ -297,11 +297,10 @@ def test_forecast_toggle(self): def test_bracket_access_by_name(self): mme = MultiMixEnvironment(PATH_DATA_MULTIMIX, _test=True) - mix1_env = mme["case14_001"] - assert mix1_env.name == "case14_001" + assert mix1_env.multimix_mix_name == "case14_001" mix2_env = mme["case14_002"] - assert mix2_env.name == "case14_002" + assert mix2_env.multimix_mix_name == "case14_002" with self.assertRaises(KeyError): unknown_env = mme["unknown_raise"] @@ -312,7 +311,7 @@ def test_keys_access(self): mix = mme[k] assert mix is not None assert isinstance(mix, BaseEnv) - assert mix.name == k + assert mix.multimix_mix_name == k def test_values_access(self): mme = MultiMixEnvironment(PATH_DATA_MULTIMIX, _test=True) @@ -320,7 +319,7 @@ def test_values_access(self): for v in mme.values(): assert v is not None assert isinstance(v, BaseEnv) - assert v == mme[v.name] + assert v == mme[v.multimix_mix_name] def test_values_unique(self): mme = MultiMixEnvironment(PATH_DATA_MULTIMIX, _test=True) diff --git a/grid2op/tests/test_PandaPowerBackendDefaultFunc.py b/grid2op/tests/test_PandaPowerBackendDefaultFunc.py index 847f1d3bb..33a290119 100644 --- a/grid2op/tests/test_PandaPowerBackendDefaultFunc.py +++ b/grid2op/tests/test_PandaPowerBackendDefaultFunc.py @@ -64,7 +64,7 @@ def get_topo_vect(self): """ otherwise there are some infinite recursions """ - res = np.full(self.dim_topo, fill_value=np.NaN, dtype=dt_int) + res = np.full(self.dim_topo, fill_value=-1, dtype=dt_int) line_status = np.concatenate( ( diff --git a/grid2op/tests/test_Runner.py b/grid2op/tests/test_Runner.py index 13994082b..0bca1dc73 100644 --- a/grid2op/tests/test_Runner.py +++ b/grid2op/tests/test_Runner.py @@ -13,6 +13,7 @@ import pdb import packaging from packaging import version +import inspect from grid2op.tests.helper_path_test import * @@ -637,6 +638,28 @@ def test_legal_ambiguous_nofaststorage(self): assert ep_data.ambiguous[1] assert not ep_data.ambiguous[2] assert not ep_data.ambiguous[3] + + def test_get_params(self): + """test the runner._get_params() function (used in multiprocessing context) + can indeed make a runner with all its arguments modified (proper 'copy' of the runner) + """ + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + env = grid2op.make("l2rpn_case14_sandbox", test=True, chronics_class=ChangeNothing, + _add_to_name=type(self).__name__) + + runner = Runner(**env.get_params_for_runner(), agentClass=AgentTestLegalAmbiguous) + made_params = runner._get_params() + ok_params = inspect.signature(Runner.__init__).parameters + for k in made_params.keys(): + assert k in ok_params, f"params {k} is returned in runner._get_params() but cannot be used to make a runner" + + for k in ok_params.keys(): + if k == "self": + continue + assert k in made_params, f"params {k} is used to make a runner but is not returned in runner._get_params()" + + if __name__ == "__main__": diff --git a/grid2op/tests/test_action_set_orig_state_options.py b/grid2op/tests/test_action_set_orig_state_options.py index 8e142a302..e42dcf680 100644 --- a/grid2op/tests/test_action_set_orig_state_options.py +++ b/grid2op/tests/test_action_set_orig_state_options.py @@ -380,10 +380,9 @@ def test_run_two_eps_seq_two_acts(self, nb_process=1): {"set_line_status": [(1, 1)], "method": "ignore"}], episode_id=[0, 1], max_iter=self.max_iter, - add_detailed_output=True, + add_detailed_output=True, # TODO HERE HERE nb_process=nb_process ) - # check for ep 0 ep_data = res[0][-1] init_obs = ep_data.observations[0] @@ -404,7 +403,7 @@ def test_run_two_eps_seq_two_acts(self, nb_process=1): {"set_line_status": [(1, 1)], "method": "ignore"}), episode_id=[0, 1], max_iter=self.max_iter, - add_detailed_output=True, + add_detailed_output=True, nb_process=nb_process ) # check for ep 0 diff --git a/grid2op/tests/test_chronics_npy.py b/grid2op/tests/test_chronics_npy.py index f1173a980..7bf98ee11 100644 --- a/grid2op/tests/test_chronics_npy.py +++ b/grid2op/tests/test_chronics_npy.py @@ -29,7 +29,9 @@ def setUp(self): self.env_name = "l2rpn_case14_sandbox" with warnings.catch_warnings(): warnings.filterwarnings("ignore") - self.env_ref = grid2op.make(self.env_name, test=True, _add_to_name=type(self).__name__) + self.env_ref = grid2op.make(self.env_name, + test=True, + _add_to_name=type(self).__name__) self.load_p = 1.0 * self.env_ref.chronics_handler.real_data.data.load_p self.load_q = 1.0 * self.env_ref.chronics_handler.real_data.data.load_q @@ -105,7 +107,7 @@ def test_proper_start_end_2(self): ), f"error at iteration {ts}" obs, *_ = env.step(env.action_space()) assert np.all(obs_ref.gen_p == obs.gen_p), f"error at iteration {ts}" - assert obs.max_step == END + assert obs.max_step == END - LAG, f"{obs.max_step} vs {END - LAG}" with self.assertRaises(Grid2OpException): env.step( env.action_space() diff --git a/grid2op/tests/test_generate_classes.py b/grid2op/tests/test_generate_classes.py index f88cdcfd8..981592485 100644 --- a/grid2op/tests/test_generate_classes.py +++ b/grid2op/tests/test_generate_classes.py @@ -20,11 +20,12 @@ class TestGenerateFile(unittest.TestCase): def _aux_assert_exists_then_delete(self, env): if isinstance(env, MultiMixEnvironment): - for mix in env: - self._aux_assert_exists_then_delete(mix) + # for mix in env: + # self._aux_assert_exists_then_delete(mix) + self._aux_assert_exists_then_delete(env.mix_envs[0]) elif isinstance(env, Environment): path = Path(env.get_path_env()) / "_grid2op_classes" - assert path.exists() + assert path.exists(), f"path {path} does not exists" shutil.rmtree(path, ignore_errors=True) else: raise RuntimeError("Unknown env type") @@ -37,33 +38,37 @@ def list_env(self): def test_can_generate(self): for env_nm in self.list_env(): - with warnings.catch_warnings(): - warnings.filterwarnings("ignore") - env = grid2op.make(env_nm, test=True, _add_to_name=type(self).__name__+"test_generate") - env.generate_classes() - self._aux_assert_exists_then_delete(env) - env.close() + try: + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + env = grid2op.make(env_nm, test=True, _add_to_name=type(self).__name__+"test_generate") + env.generate_classes() + finally: + self._aux_assert_exists_then_delete(env) + env.close() def test_can_load(self): + _add_to_name = type(self).__name__+"test_load" for env_nm in self.list_env(): with warnings.catch_warnings(): warnings.filterwarnings("ignore") - env = grid2op.make(env_nm, test=True, _add_to_name=type(self).__name__+"_TestGenerateFile") + env = grid2op.make(env_nm, + test=True, + _add_to_name=_add_to_name) env.generate_classes() - with warnings.catch_warnings(): warnings.filterwarnings("ignore") try: env2 = grid2op.make(env_nm, test=True, experimental_read_from_local_dir=True, - _add_to_name=type(self).__name__) + _add_to_name=_add_to_name) env2.close() except RuntimeError as exc_: raise RuntimeError(f"Error for {env_nm}") from exc_ self._aux_assert_exists_then_delete(env) env.close() + if __name__ == "__main__": unittest.main() - \ No newline at end of file diff --git a/grid2op/tests/test_gym_asynch_env.py b/grid2op/tests/test_gym_asynch_env.py index e0cca4c75..c9eb7eb1d 100644 --- a/grid2op/tests/test_gym_asynch_env.py +++ b/grid2op/tests/test_gym_asynch_env.py @@ -181,5 +181,6 @@ def setUp(self) -> None: self.skipTest("Not handled at the moment") return super().setUp() + if __name__ == "__main__": unittest.main() diff --git a/grid2op/tests/test_issue_196.py b/grid2op/tests/test_issue_196.py index c6a4b815d..08f5987d5 100644 --- a/grid2op/tests/test_issue_196.py +++ b/grid2op/tests/test_issue_196.py @@ -49,3 +49,7 @@ def test_issue_196_genp(self): # not great test as it passes with the bug... but just in the case... cannot hurt obs, *_ = self.env_gym.reset() assert obs in self.env_gym.observation_space + + +if __name__ == "__main__": + unittest.main() diff --git a/grid2op/tests/test_pickling.py b/grid2op/tests/test_pickling.py index ea262d583..c8114d93e 100644 --- a/grid2op/tests/test_pickling.py +++ b/grid2op/tests/test_pickling.py @@ -20,13 +20,17 @@ ScalerAttrConverter, ) +_NAME_FOR_THIS_TEST = __name__ + "for_mp_test" + with warnings.catch_warnings(): # this needs to be imported in the main module for multiprocessing to work "approximately" warnings.filterwarnings("ignore") - _ = grid2op.make("l2rpn_case14_sandbox", test=True, _add_to_name=__name__+"for_mp_test") - - + _ = grid2op.make("l2rpn_case14_sandbox", + test=True, + _add_to_name=_NAME_FOR_THIS_TEST) + + class TestMultiProc(unittest.TestCase): @staticmethod def f(env_gym): @@ -41,7 +45,9 @@ def test_basic(self): with warnings.catch_warnings(): warnings.filterwarnings("ignore") env = grid2op.make( - "l2rpn_case14_sandbox", test=True, _add_to_name=__name__+"for_mp_test" + "l2rpn_case14_sandbox", + test=True, + _add_to_name=_NAME_FOR_THIS_TEST ) env_gym = GymEnv(env) @@ -71,15 +77,15 @@ def test_basic(self): ["rho", "gen_p", "load_p", "topo_vect", "actual_dispatch"] ) ob_space = ob_space.reencode_space( - "actual_dispatch", ScalerAttrConverter(substract=0.0, divide=env.gen_pmax) + "actual_dispatch", ScalerAttrConverter(substract=0.0, divide=1. * type(env).gen_pmax) ) ob_space = ob_space.reencode_space( - "gen_p", ScalerAttrConverter(substract=0.0, divide=env.gen_pmax) + "gen_p", ScalerAttrConverter(substract=0.0, divide=1. * type(env).gen_pmax) ) ob_space = ob_space.reencode_space( "load_p", ScalerAttrConverter( - substract=obs_gym["load_p"], divide=0.5 * obs_gym["load_p"] + substract=1. * obs_gym["load_p"], divide=0.5 * obs_gym["load_p"] ), ) env_gym.observation_space = ob_space @@ -95,4 +101,11 @@ def test_basic(self): if __name__ == "__main__": + with warnings.catch_warnings(): + # this needs to be imported in the main module for multiprocessing to work "approximately" + warnings.filterwarnings("ignore") + _ = grid2op.make("l2rpn_case14_sandbox", + test=True, + _add_to_name=__name__+"for_mp_test") + unittest.main() diff --git a/setup.py b/setup.py index ec6d9f963..db3c36bf2 100644 --- a/setup.py +++ b/setup.py @@ -84,7 +84,6 @@ def my_test_suite(): } pkgs["extras"]["test"] += pkgs["extras"]["optional"] pkgs["extras"]["test"] += pkgs["extras"]["plot"] -pkgs["extras"]["test"] += pkgs["extras"]["chronix2grid"] pkgs["extras"]["test"] += pkgs["extras"]["gymnasium"] if sys.version_info.minor <= 7: