How to add a new attribute to the observation

Work in progress !

The best things to do if you want to add attributes to the observation is to create a class, derived from CompleteObservation and to add attributes in:

  • the attr_list_vect class attribute

  • the attr_vect_cpy / attr_simple_cpy class attribute (depending of if the attributes are np.array or more simple attribute like float or bool)

And then you create an environment with your action class.

An example is given in the https://github.com/Grid2op/grid2op/tree/master/examples/backend_dependant_code example.

Danger

These examples works for grid2op >= 1.12. Upgrade grid2op if you need this feature.

Warning

In all cases, we recommend to do some extensive tests before using your new class in an experiment using grid2op (that could last very long times)

And once validated and everything works perfectly in a consistent manner, use your feature as much as you want.

Most simple setting

For example, suppose you want to add something computed by the environment but not part of the observation.

For the sake of this example let’s assume the environment computes something calls “hidden_stuff” (an integer), so env.hidden_stuff is an existing variable, but obs.hidden_stuff does not exists (yet).

But for some reason, you decide that an agent should have access to this env.hidden_stuff.

You can extend grid2op quite easily in this case.

First you create the Observation class that you want:

from grid2op.Environment import BaseEnv
from grid2op.Observation import CompleteObservation

class ObsWithHiddenStuff(CompleteObservation):
    # attributes that will be saved when action is
    # serialized as a numpy vector
    attr_list_vect = copy.deepcopy(CompleteObservation.attr_list_vect)
    attr_list_vect.append("hidden_stuff")

    # attributes that will also be used when action is
    # serialized as json
    attr_list_json = copy.deepcopy(CompleteObservation.attr_list_json)

    # attributes that will be copied
    # when observation is copied
    attr_simple_cpy  = copy.deepcopy(CompleteObservation.attr_simple_cpy)
    attr_simple_cpy.append("hidden_stuff")

    def __init__(self,
                 obs_env=None,
                 action_helper=None,
                 random_prng=None,
                 kwargs_env=None,
    )
        super.__init__(
            obs_env=obs_env,
            action_helper=action_helper,
            random_prng=random_prng,
            kwargs_env=kwargs_env)
        self.hidden_stuff = None

    def update(self, env: BaseEnv, with_forecast=True):
        # update standard attribute
        super().update(env, with_forecast=with_forecast)
        self.hidden_stuff = copy.copy(env.hidden_stuff)

And that is it.

Now, you need to tell grid2op to use this class, and for that you can do the following:

import grid2op

env_name = "l2rpn_case14_sandbox"
env = grid2op.make(env_name,
                   observation_class=ObsWithHiddenStuff
                   )

obs = env.reset()

print(f"New things: {obs.hidden_stuff}")

Warning

This is a fictious example, there is no attribute “hidden_stuff” in the environment, so this will NOT work.

This is, again, just an example.

Adding a vector attribute

A step a bit more complicated would be to add “vector” attribute to the observation.

It is quite similar to the previous things, but instead of setting attr_simple_cpy class attribute, you modify attr_vect_cpy

This gives something like:

from grid2op.Environment import BaseEnv
from grid2op.Observation import CompleteObservation

class ObsWithHiddenStuffVector(CompleteObservation):
    # attributes that will be saved when action is
    # serialized as a numpy vector
    attr_list_vect = copy.deepcopy(CompleteObservation.attr_list_vect)
    attr_list_vect.append("hidden_stuff_vect")

    # attributes that will also be used when action is
    # serialized as json
    attr_list_json = copy.deepcopy(CompleteObservation.attr_list_json)

    # attributes that will be copied
    # when observation is copied
    attr_vect_cpy  = copy.deepcopy(CompleteObservation.attr_vect_cpy)
    attr_vect_cpy.append("hidden_stuff_vect")

    def __init__(self,
                 obs_env=None,
                 action_helper=None,
                 random_prng=None,
                 kwargs_env=None,
    )
        super.__init__(
            obs_env=obs_env,
            action_helper=action_helper,
            random_prng=random_prng,
            kwargs_env=kwargs_env)

        # in this case it is required to know the size
        # when initializing an observation.
        # if you want attribute with dynamic size, you can
        # 1) write a github issue explaining your usecase so
        #    that a good solution is found
        # 2) use "0-padding" strategy and set the vector
        #    to the maximum size
        # 3) mark your vector as "simple" and not worry about the
        #    size at all (so setting attr_simple_cpy instead of attr_vect_cpy)
        self.hidden_stuff_vect = np.zeros(42, dtype=...)

    def update(self, env: BaseEnv, with_forecast=True):
        # update standard attribute
        super().update(env, with_forecast=with_forecast)
        self.hidden_stuff_vect[:] = copy.copy(env.hidden_stuff_vect)

And that is it.

Note

You can also set attr_simple_cpy in this case. The difference is that it will be less efficient, especially when observation are copied.

Observation can be copied multiple times in a single step

Adding attributes non trivial attribute

In the previous section, we explained how to add attributes that were already available in the environment.

But this feature can be used to retrieve more information, for example to compute other (or retrieve) other type of attributes.

Generic Observation attributes

You can dive a bit deeper and use this feature to compute values or extract values from the environment’s backend.

This is what is actually done in the example at https://github.com/Grid2op/grid2op/tree/master/examples/backend_dependant_code were the backend computes some flow results based on n-1 computation.

Feel free to consult the documentation there for more information (duplicating code is the best way to add typo, so best keep the example in a single place :-) )

The way it is coded is “generic” in the sens that it relies only on grid2op features. So nothing dramatic should happen if you use it correctly.

Backend Dependent Observation attribtues

You can also use this feature to add to the observation physical properties of grid equipment, for example the physical properties of powerlines.

In this later case, the observation will be “backend dependant” (you will not be able to use it with other grid2op backend, but that is totally fine, as long as you are fine with the consequences). A code to get the “tap position” of the transformer when using the PandaPowerBackend would look something like:

from grid2op.Environment import BaseEnv
from grid2op.Observation import CompleteObservation

class ObsWithTrafoTapPos(CompleteObservation):
    # attributes that will be saved when action is
    # serialized as a numpy vector
    attr_list_vect = copy.deepcopy(CompleteObservation.attr_list_vect)
    attr_list_vect.append("pp_tap_position")

    # attributes that will also be used when action is
    # serialized as json
    attr_list_json = copy.deepcopy(CompleteObservation.attr_list_json)

    # attributes that will be copied
    # when observation is copied
    attr_vect_cpy  = copy.deepcopy(CompleteObservation.attr_vect_cpy)
    attr_vect_cpy.append("pp_tap_position")

    def __init__(self,
                 obs_env=None,
                 action_helper=None,
                 random_prng=None,
                 kwargs_env=None,
    )
        super.__init__(
            obs_env=obs_env,
            action_helper=action_helper,
            random_prng=random_prng,
            kwargs_env=kwargs_env)

        # in this case it is required to know the size
        # when initializing an observation.
        # if you want attribute with dynamic size, you can
        # 1) write a github issue explaining your usecase so
        #    that a good solution is found
        # 2) use "0-padding" strategy and set the vector
        #    to the maximum size
        # 3) mark your vector as "simple" and not worry about the
        #    size at all (so setting attr_simple_cpy instead of attr_vect_cpy)
        self.pp_tap_position = np.zeros(type(self).n_line, dtype=int)

    def update(self, env: BaseEnv, with_forecast=True):
        # update standard attribute
        super().update(env, with_forecast=with_forecast)
        pp_grid = env.backend._grid  # of course this works only with pandapower !
        n_powerline = pp_grid.line.shape[0]
        self.pp_tap_position[n_powerline:] = copy.copy(pp_grid.trafo["tap_pos"].values)
        # I know this will work because we know that PandaPowerBackend orders
        # first the powerlines (where we put a tap of 0 by default here)
        # and then the trafos. So we only need to update the last elements for the trafos

If you did not follow exactly what is happening here it’s perfectly fine. It probably means that either pandapower or the PandaPowerBackend still have secrets for you. The idea is simply to tell you that, in the “update” definition you can use env.backend._grid to perform whatever you want.

Side effects and limitations

If you use backend dependant code, your observation class will only be usable with a given backend. It is also preferable that you are really familiar with the backend if you want to do correct things.

We also recommend to copy things from the backend to the observation (see the use of copy.copy AND of [:] in the example above). This is to prevent any “unwanted modifications” when the agent reads back the observation. For example, we would not want that obs.pp_tap_position[10] = 3 somewhere in the code modifies the backend internal state.

And, as for the “act.backend_dependant_callback”, “with a great power comes great responsibility”. You have total control on the internal state of the backend here. So you can modify it without grid2op knowing it. This means that if you perform some “weird” things you can break almost everything on grid2op, and you can do so without grid2op raising any error or exception.

You might even do some modification that will be kept for an entire episode and even after a “env.reset”.

Danger

If you want to avoid any side effect, please consider that env.backend in general and env.backend._grid in particular are “read only” attributes. It’s fine to copy them, look at them etc.

But we would not recommend to modify them directly, let alone in the “observation” class. If you want a direct control on them, you can use the grid2op.Action.BaseAction.backend_dependant_callback instead.

Observation class parametrized at their creation

You can also have your observation class depends on some parameters.

This is for example the case with the grid2op.Observation.NoisyObservation

If you want to use such a class, you need to :

  • the attr_list_vect class attribute

  • the attr_vect_cpy / attr_simple_cpy class attribute (depending of if the attributes are np.array or more simple attribute like float or bool)

  • pass the extra key-word arguments (kwargs) to the init of the BaseObservation class.

This looks something like this (code taken from https://github.com/Grid2op/grid2op/tree/master/examples/backend_dependant_code):

class ObsWithN1(CompleteObservation):
    # attributes that will be saved when action is
    # serialized as a numpy vector
    attr_list_vect = copy.deepcopy(CompleteObservation.attr_list_vect)
    attr_list_vect.append("n1_vals")

    # attributes that will also be used when action is
    # serialized as json
    attr_list_json = copy.deepcopy(CompleteObservation.attr_list_json)

    # attributes that will be copied
    # when observation is copied
    attr_vect_cpy  = copy.deepcopy(CompleteObservation.attr_vect_cpy)
    attr_vect_cpy.append("n1_vals")

    def __init__(self,
                obs_env=None,
                action_helper=None,
                random_prng=None,
                kwargs_env=None,
                n1_li=None,  # kwargs not present in CompleteObservation
                reduce_n1: Literal["max", "count", "sum"]="max", # kwargs not present in CompleteObservation
                compute_algo: Literal["ac", "dc"]="ac"): # kwargs not present in CompleteObservation
        super().__init__(obs_env,
                        action_helper,
                        random_prng,
                        kwargs_env,
                        n1_li=n1_li,  # add the extra kwargs in the init here
                        reduce_n1=reduce_n1,  # add the extra kwargs in the init here
                        compute_algo=compute_algo  # add the extra kwargs in the init here
                        )
        ...

    def update(...):
        ...

You need to add the constructor in the init otherwise they will be “lost” when the observations are copied for example. So basically they will never be used.

If you still can’t find what you’re looking for, try in one of the following pages:

Still trouble finding the information ? Do not hesitate to send a github issue about the documentation at this link: Documentation issue template

Copyright © Grid2Op a Series of LF Projects, LLC For website terms of use, trademark policy and other project policies please see https://lfprojects.org.