Source code for smartgrid.world

"""
The World represents the "physical" (simulated) smart grid.
"""

from typing import Iterable, Dict

import numpy as np

from smartgrid.agents import Agent
from smartgrid.util import EnergyGenerator


[docs] class World(object): """ Represents the "physical" (simulated) smart grid. As per the Gym framework, our *World* represents the smart grid, and manages interactions within agents in the smart grid. It handles transitions between time steps, i.e., simulating the changes that happen, which are provoked by the agents' actions and the smart grid dynamics (energy generation, etc.). """ agents_by_name: Dict[str, Agent] """ Agents in the World, indexed by their name. This allows efficient access to a specific :py:class:`~smartgrid.agent.Agent` based on its :py:attr:`~smartgrid.agents.agent.Agent.name`. Agents represent *prosumers* (buildings) that act in this World (by consuming and exchanging energy). .. note:: There is (currently) no support for "scripted" agents, i.e., all agents in the world are considered to be "policy" agents: they receive observations and algorithms must decide actions for them, through the environment interaction loop (see :py:class:`.SmartGrid` for more details). .. note:: Adding agents is currently not supported in this version. """ current_step: int """ Current time step of the world. Each time step corresponds to a simulated hour, which the world keeps track of. The world is also responsible for incrementing the time steps, and simulating the next step. Initially, ``current_step`` is set to ``0``. """ available_energy: int """ Current quantity of available energy in the local grid. At each time step, the smart grid generates an important quantity of energy that is accessible to all agents. This quantity is assumed to come from an important but local source, such as a windmill farm, or an hydraulic power plant. It is separated from the *national grid*, which is considered unlimited, but must be paid by agents. On the contrary, the smart grid's *available energy* is free, but limited, and agents must learn not to consume too much so as to let energy for others. See :py:attr:`.energy_generator` for more details on how the *available energy* is generated at each step. """ energy_generator: EnergyGenerator """ Generator of energy for each time step, at the smart grid level. We recall that the smart grid locally generates an important quantity of energy accessible to all agents. In order to make this generation agnostic to the number and profiles of agents, the *energy generators* rely on the maximum energy needed by all agents. For example, consider 3 Households agents with a maximal need of 10kWh each. The maximum energy needed in the whole world will thus be 30kWh. A generator may produce between 80% and 120% of this maximal need, which means that in some cases, there is not enough energy for all agents, and they must reduce consumption (or risk preventing others from consuming), and in other cases, there is more energy than necessary, and they can store energy for later. See :py:class:`.EnergyGenerator` for more details. """
[docs] def __init__(self, agents: Iterable[Agent], energy_generator: EnergyGenerator): """ Create a new simulated world. :param agents: The list of agents that partake in this smart grid. :param energy_generator: The generator used to produce energy at each time step, based on the agents in the world and their needs. """ self.current_step = 0 self.agents_by_name = { agent.name: agent for agent in agents } self.available_energy = 0 self.energy_generator = energy_generator
[docs] def step(self): """ Perform a new step of the simulation. This function performs the following: 1. Actions are truly enacted. They were "intended" before, i.e., agents output a *decision*; now they are applied to the world, and the world is updated accordingly. For exemple by updating the agents' payoffs, their battery, the available energy, and so on. 2. Agents are updated. They generate a new need, a new production, and they compute their comfort. 3. A new ``available_energy`` is generated, based on the (new) agents' needs. """ # 1. Integrate all agents' actions for agent in self.agents: agent.enacted_action = agent.handle_action() # 2. Update agents self.current_step += 1 for agent in self.agents: agent.update(self.current_step) # 3. Generate new "available energy" self.available_energy = self.energy_generator.generate_available_energy( self.current_need, self.current_step, self.min_needed_energy, self.max_needed_energy )
[docs] def reset(self, random_generator: np.random.Generator = None): """ Resets the state of the world to the initial state. This resets the current step, the observation manager, the agents themselves, and the available energy. This function must be called when initializing the world. :param random_generator: The NumPy random generator, for reproducibility purposes. This is automatically handled by the SmartGrid :py:meth:`~smartgrid.environment.SmartGrid.reset` method. By default (to facilitate using the World and to avoid breaking the API), it will be set to a new Random Generator. """ self.current_step = 0 for agent in self.agents: agent.reset() if random_generator is None: random_generator = np.random.default_rng() self.energy_generator.set_random_generator(random_generator) self.available_energy = self.energy_generator.generate_available_energy( self.current_need, self.current_step, self.min_needed_energy, self.max_needed_energy )
@property def agents(self) -> Iterable[Agent]: """ Iterable of all agents acting in the world. This iterable can be used to iterate on all agents, when accessing them by their name is not required (see :py:attr:`.agents_by_name` for this). It is internally set as a *view* on the dictionary's :py:meth:`dict.values`. """ return self.agents_by_name.values() @property def current_need(self): """ The total energy that agents currently need, for this time step. For now computed on-demand, but may be stored as an attribute in the future, to avoid computations. """ return sum([a.need for a in self.agents]) @property def min_needed_energy(self): """ The minimum sum of energy that all agents need, for all time steps. Currently simplified to return 0, may be more accurate in the future. """ return 0 @property def max_needed_energy(self): """The total amount of energy that all agents need. It can be used for example to interpolate the current amount of available energy to [0,1]. This maximum amount depends on the list of current agents, especially the maximum amount of energy that each may need. """ return sum([agent.profile.max_energy_needed for agent in self.agents]) def __str__(self): return '<World t={}>'.format(self.current_step)