Skip to content

Commit

Permalink
Merge pull request #444 from BDonnot/master
Browse files Browse the repository at this point in the history
Add 2 new features for the forecast_env
  • Loading branch information
BDonnot committed Apr 24, 2023
2 parents b9e1ac3 + 536b347 commit d418924
Show file tree
Hide file tree
Showing 9 changed files with 639 additions and 102 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,8 @@ test_vecenv.7z
test_vecenv.py
test_pp_networks.py
test_timeout_env.py
test_pp_dc.py
et_trace()

# profiling files
**.prof
5 changes: 5 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ Change Log
- [FIXED] a bug n the `GridStateFromFile`, `GridStateFromFileWithForecasts` and
`GridStateFromFileWithForecastsWithoutMaintenance` classes that caused the maintenance file to be
ignored when "chunk_size" was set.
- [FIXED] a bug when shunts were alone in `backend.check_kirchoff()`
- [ADDED] the function `obs.get_forecast_env()` that is able to generate a grid2op environment from the
forecasts data in the observation. This is especially useful in model based RL.
- [ADDED] an example on how to write a backend.
Expand All @@ -83,6 +84,10 @@ Change Log
- [ADDED] a method to retrieve the "elements graph" (see doc) fom an observation `obs.get_elements_graph()`
- [ADDED] a whole new way to deal with input time series data (see the module `grid2op.Chronics.handlers`
for more information)
- [ADDED] possibility to change the parameters used for the `obs.simulate(...)`
directly from the grid2op action, see `obs.change_forecast_parameters()`
- [ADDED] possibility to retrieve a "forecast environment" with custom forecasts, see
`obs.get_env_from_external_forecasts(...)`
- [IMPROVED] possibility to "chain" the call to simulate when multiple forecast
horizon are available.
- [IMPROVED] the `GridStateFromFileWithForecasts` is now able to read forecast from multiple steps
Expand Down
28 changes: 27 additions & 1 deletion docs/model_based.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,26 @@ There are 3 standard methods currently in grid2op to apply "model based" / "plan
1) use "obs.simulate" (see :func:`grid2op.Observation.BaseObservation.simulate`)
2) use the "Simulator" (see :ref:`simulator_page` and :mod:`grid2op.simulator`)
3) use the "forecast env" (see :func:`grid2op.Observation.BaseObservation.get_forecast_env`)
4) use the "forecast env" (see :func:`grid2op.Observation.BaseObservation.get_env_from_external_forecasts`)

.. note::
The main difference between :func:`grid2op.Observation.BaseObservation.get_forecast_env`
and :func:`grid2op.Observation.BaseObservation.get_env_from_external_forecasts`
is that the first one rely on provided forecast in the environment
and in :func:`grid2op.Observation.BaseObservation.get_env_from_external_forecasts`
you are responsible for providing these forecasts.

This has some implications:

- you cannot use `obs.get_forecast_env()` if the forecasts are deactivated,
or if there are no provided forecast in the environment
- the number of steps possible in `obs.get_forecast_env()` is fixed and determined
by the environment.
- `"garbarge in" = "garbage out"` is especially true for `obs.get_env_from_external_forecasts`
By this I mean that if you provided forecasts with poor quality (*eg* that does
not contain any usefull information about the future, or such that the total generation is
lower that the total demand etc.) then you will most likely not get any usefull information
from their usage.

And you can use them for different strategies among:

Expand Down Expand Up @@ -276,7 +296,13 @@ Forecast env
---------------

Finally you can use the :func:`grid2op.Observation.BaseObservation.get_forecast_env` to retrieve an actual
environment already loaded with the "forecast" data available.
environment already loaded with the "forecast" data available. Alternatively,
if you want to use this feature but the environment does not provide such forecasts
you can have a look at the
:func:`grid2op.Observation.BaseObservation.get_env_from_external_forecasts`
(if you can generate your own forecasts) or
the :ref:`tshandler-module` section of the documentation (to still be able
to "generate" forecasts)

Lots of example can be use in this setting, for example using MCTS or any other "planning strategy", but if we take
again the example of the section :ref:`mb_simulate` above this also allows to evaluate the impact of
Expand Down
186 changes: 93 additions & 93 deletions grid2op/Backend/PandaPowerBackend.py
Original file line number Diff line number Diff line change
Expand Up @@ -1024,7 +1024,7 @@ def runpf(self, is_dc=False):
)

if is_dc:
pp.rundcpp(self._grid, check_connectivity=False)
pp.rundcpp(self._grid, check_connectivity=False, init="flat")
self._nb_bus_before = (
None # if dc i start normally next time i call an ac powerflow
)
Expand All @@ -1048,97 +1048,98 @@ def runpf(self, is_dc=False):
# sometimes pandapower does not detect divergence and put Nan.
raise pp.powerflow.LoadflowNotConverged("Divergence due to Nan values in res_gen table.")

(
self.prod_p[:],
self.prod_q[:],
self.prod_v[:],
self.gen_theta[:],
) = self._gens_info()
(
self.load_p[:],
self.load_q[:],
self.load_v[:],
self.load_theta[:],
) = self._loads_info()
if not is_dc:
if not np.all(np.isfinite(self.load_v)):
# TODO see if there is a better way here
# some loads are disconnected: it's a game over case!
raise pp.powerflow.LoadflowNotConverged("Isolated load")
else:
# fix voltages magnitude that are always "nan" for dc case
# self._grid.res_bus["vm_pu"] is always nan when computed in DC
self.load_v[:] = self.load_pu_to_kv # TODO
# need to assign the correct value when a generator is present at the same bus
# TODO optimize this ugly loop
for l_id in range(self.n_load):
if self.load_to_subid[l_id] in self.gen_to_subid:
ind_gens = np.where(
self.gen_to_subid == self.load_to_subid[l_id]
)[0]
for g_id in ind_gens:
if (
self._topo_vect[self.load_pos_topo_vect[l_id]]
== self._topo_vect[self.gen_pos_topo_vect[g_id]]
):
self.load_v[l_id] = self.prod_v[g_id]
break

self.line_status[:] = self._get_line_status()
# I retrieve the data once for the flows, so has to not re read multiple dataFrame
self.p_or[:] = self._aux_get_line_info("p_from_mw", "p_hv_mw")
self.q_or[:] = self._aux_get_line_info("q_from_mvar", "q_hv_mvar")
self.v_or[:] = self._aux_get_line_info("vm_from_pu", "vm_hv_pu")
self.a_or[:] = self._aux_get_line_info("i_from_ka", "i_hv_ka") * 1000
self.theta_or[:] = self._aux_get_line_info(
"va_from_degree", "va_hv_degree"
)
self.a_or[~np.isfinite(self.a_or)] = 0.0
self.v_or[~np.isfinite(self.v_or)] = 0.0

self.p_ex[:] = self._aux_get_line_info("p_to_mw", "p_lv_mw")
self.q_ex[:] = self._aux_get_line_info("q_to_mvar", "q_lv_mvar")
self.v_ex[:] = self._aux_get_line_info("vm_to_pu", "vm_lv_pu")
self.a_ex[:] = self._aux_get_line_info("i_to_ka", "i_lv_ka") * 1000
self.theta_ex[:] = self._aux_get_line_info(
"va_to_degree", "va_lv_degree"
(
self.prod_p[:],
self.prod_q[:],
self.prod_v[:],
self.gen_theta[:],
) = self._gens_info()
(
self.load_p[:],
self.load_q[:],
self.load_v[:],
self.load_theta[:],
) = self._loads_info()
if not is_dc:
if not np.all(np.isfinite(self.load_v)):
# TODO see if there is a better way here
# some loads are disconnected: it's a game over case!
raise pp.powerflow.LoadflowNotConverged("Isolated load")
else:
# fix voltages magnitude that are always "nan" for dc case
# self._grid.res_bus["vm_pu"] is always nan when computed in DC
self.load_v[:] = self.load_pu_to_kv # TODO
# need to assign the correct value when a generator is present at the same bus
# TODO optimize this ugly loop
# see https://github.com/e2nIEE/pandapower/issues/1996 for a fix
for l_id in range(self.n_load):
if self.load_to_subid[l_id] in self.gen_to_subid:
ind_gens = np.where(
self.gen_to_subid == self.load_to_subid[l_id]
)[0]
for g_id in ind_gens:
if (
self._topo_vect[self.load_pos_topo_vect[l_id]]
== self._topo_vect[self.gen_pos_topo_vect[g_id]]
):
self.load_v[l_id] = self.prod_v[g_id]
break

self.line_status[:] = self._get_line_status()
# I retrieve the data once for the flows, so has to not re read multiple dataFrame
self.p_or[:] = self._aux_get_line_info("p_from_mw", "p_hv_mw")
self.q_or[:] = self._aux_get_line_info("q_from_mvar", "q_hv_mvar")
self.v_or[:] = self._aux_get_line_info("vm_from_pu", "vm_hv_pu")
self.a_or[:] = self._aux_get_line_info("i_from_ka", "i_hv_ka") * 1000
self.theta_or[:] = self._aux_get_line_info(
"va_from_degree", "va_hv_degree"
)
self.a_or[~np.isfinite(self.a_or)] = 0.0
self.v_or[~np.isfinite(self.v_or)] = 0.0

self.p_ex[:] = self._aux_get_line_info("p_to_mw", "p_lv_mw")
self.q_ex[:] = self._aux_get_line_info("q_to_mvar", "q_lv_mvar")
self.v_ex[:] = self._aux_get_line_info("vm_to_pu", "vm_lv_pu")
self.a_ex[:] = self._aux_get_line_info("i_to_ka", "i_lv_ka") * 1000
self.theta_ex[:] = self._aux_get_line_info(
"va_to_degree", "va_lv_degree"
)
self.a_ex[~np.isfinite(self.a_ex)] = 0.0
self.v_ex[~np.isfinite(self.v_ex)] = 0.0

# it seems that pandapower does not take into account disconencted powerline for their voltage
self.v_or[~self.line_status] = 0.0
self.v_ex[~self.line_status] = 0.0
self.v_or[:] *= self.lines_or_pu_to_kv
self.v_ex[:] *= self.lines_ex_pu_to_kv

# see issue https://github.com/rte-france/Grid2Op/issues/389
self.theta_or[~np.isfinite(self.theta_or)] = 0.0
self.theta_ex[~np.isfinite(self.theta_ex)] = 0.0

self._nb_bus_before = None
self._grid._ppc["gen"][self._iref_slack, 1] = 0.0

# handle storage units
# note that we have to look ourselves for disconnected storage
(
self.storage_p[:],
self.storage_q[:],
self.storage_v[:],
self.storage_theta[:],
) = self._storages_info()
deact_storage = ~np.isfinite(self.storage_v)
if np.any(np.abs(self.storage_p[deact_storage]) > self.tol):
raise pp.powerflow.LoadflowNotConverged(
"Isolated storage set to absorb / produce something"
)
self.a_ex[~np.isfinite(self.a_ex)] = 0.0
self.v_ex[~np.isfinite(self.v_ex)] = 0.0

# it seems that pandapower does not take into account disconencted powerline for their voltage
self.v_or[~self.line_status] = 0.0
self.v_ex[~self.line_status] = 0.0
self.v_or[:] *= self.lines_or_pu_to_kv
self.v_ex[:] *= self.lines_ex_pu_to_kv

# see issue https://github.com/rte-france/Grid2Op/issues/389
self.theta_or[~np.isfinite(self.theta_or)] = 0.0
self.theta_ex[~np.isfinite(self.theta_ex)] = 0.0

self._nb_bus_before = None
self._grid._ppc["gen"][self._iref_slack, 1] = 0.0

# handle storage units
# note that we have to look ourselves for disconnected storage
(
self.storage_p[:],
self.storage_q[:],
self.storage_v[:],
self.storage_theta[:],
) = self._storages_info()
deact_storage = ~np.isfinite(self.storage_v)
if np.any(np.abs(self.storage_p[deact_storage]) > self.tol):
raise pp.powerflow.LoadflowNotConverged(
"Isolated storage set to absorb / produce something"
)
self.storage_p[deact_storage] = 0.0
self.storage_q[deact_storage] = 0.0
self.storage_v[deact_storage] = 0.0
self._grid.storage["in_service"].values[deact_storage] = False
self.storage_p[deact_storage] = 0.0
self.storage_q[deact_storage] = 0.0
self.storage_v[deact_storage] = 0.0
self._grid.storage["in_service"].values[deact_storage] = False

self._topo_vect[:] = self._get_topo_vect()
return self._grid.converged, None
self._topo_vect[:] = self._get_topo_vect()
return self._grid.converged, None

except pp.powerflow.LoadflowNotConverged as exc_:
# of the powerflow has not converged, results are Nan
Expand Down Expand Up @@ -1502,12 +1503,11 @@ def shunt_info(self):
.values.astype(dt_float)
)
shunt_bus = type(self).global_bus_to_local(self._grid.shunt["bus"].values, self.shunt_to_subid)
shunt_v[~self._grid.shunt["in_service"].values] = 0.
shunt_v[~self._grid.shunt["in_service"].values] = 0
shunt_bus[~self._grid.shunt["in_service"].values] = -1

# handle shunt alone on a bus (in this case it should probably diverge...)
alone = ~np.isfinite(shunt_v)
shunt_v[alone] = 0.
shunt_v[alone] = 0
shunt_bus[alone] = -1
return shunt_p, shunt_q, shunt_v, shunt_bus

Expand Down

0 comments on commit d418924

Please sign in to comment.