Observation Functions#

In this section we describe the observation function protocol, the observation function registry, and how to write your own custom observation functions.

The ObservationFunction Protocol#

An observation function is a generative function which represents a (stochastic) mapping from a state to an observation. Using the typing standard library, the observation function type is defined as a callable typing.Protocol which receives a State and an optional numpy.random.Generator, and returns an Observation.

class ObservationFunction(*args, **kwargs)[source]
__call__(state, *, rng=None)[source]

Call self as a function.

Return type

Observation

Note

An observation function may (and often does) accept additional arguments; this is possible so long as the extra arguments either have default values, or are binded to specific values later on, e.g., using functools.partial().

The Observation Function Registry#

The observation_functions module contains some pre-defined observation functions and the observation_function_registry, a dictionary-like object through which to register and retrieve observation functions. Observation functions are registered using the register() method, which can be used as a decorator (also see Custom Observation Functions). As a dictionary, observation_function_registry has a keys() method which returns the names of registered functions.

Custom Observation Functions#

Note

While writing your own observation function directly is indeed a possibility, the most common way to implement new observation functions is by writing a custom VisibilityFunction and using it with the from_visibility() observation function.

Custom observation functions can be defined so long as they satisfy some basic rules; A custom observation function:

Warning

The rng argument is used to control the source of randomness and allow for the environment to be seeded via set_seed(), which in turn guarantees the reproducibility of traces, runs, and experiments; if you wish to use external sources of randomness, you will have to manage them and their seeding yourself.

Practical Example#

Note

The examples shown here can be found in the examples/ folder.

In this example, we are going to write an observation function which simulates a satellite view of the agent, i.e., a view from the top (being able to see through walls), and with the agent being off-center in each observation.

from typing import Optional

import numpy.random as rnd

from gym_gridverse.agent import Agent
from gym_gridverse.envs.observation_functions import (
    observation_function_registry,
)
from gym_gridverse.geometry import Area, Position, Shape
from gym_gridverse.observation import Observation
from gym_gridverse.rng import get_gv_rng_if_none
from gym_gridverse.state import State


@observation_function_registry.register
def satellite(
    state: State,
    *,
    shape: Shape,
    rng: Optional[rnd.Generator] = None,
) -> Observation:

    rng = get_gv_rng_if_none(rng)  # necessary to use rng object!

    # randomly sample an area of the given shape which includes the agent
    # (also avoids putting the agent on the edge)
    y0: int = rng.integers(
        state.agent.position.y - shape.height + 2,
        state.agent.position.y - 1,
        endpoint=True,
    )
    x0: int = rng.integers(
        state.agent.position.x - shape.width + 2,
        state.agent.position.x - 1,
        endpoint=True,
    )
    area = Area((y0, y0 + shape.height - 1), (x0, x0 + shape.width - 1))
    observation_grid = state.grid.subgrid(area)

    observation_agent = Agent(
        state.agent.position - Position(y0, x0),
        state.agent.orientation,
        state.agent.grid_object,
    )

    return Observation(observation_grid, observation_agent)