From 043fc30d7e0006c6d7c43c07912cc1e0e9dd39f2 Mon Sep 17 00:00:00 2001 From: BamunugeDR99 Date: Mon, 3 Jun 2024 09:42:20 +0530 Subject: [PATCH 01/11] feat:Substation Clustering using Louvain Algorithm --- examples/multi_agents/ray_example3.py | 276 ++++++++++++++++++++++++++ grid2op/multi_agent/__init__.py | 4 +- grid2op/multi_agent/utils.py | 78 +++++++- setup.py | 3 + 4 files changed, 358 insertions(+), 3 deletions(-) create mode 100644 examples/multi_agents/ray_example3.py diff --git a/examples/multi_agents/ray_example3.py b/examples/multi_agents/ray_example3.py new file mode 100644 index 000000000..4baf8f9db --- /dev/null +++ b/examples/multi_agents/ray_example3.py @@ -0,0 +1,276 @@ +# Copyright (c) 2019-2022, 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. + +"""example with centralized observation and local actions""" +import warnings +import numpy as np +import copy + +from gym.spaces import Discrete, Box + +from ray.rllib.env.multi_agent_env import MultiAgentEnv as MAEnv +from ray.rllib.policy.policy import PolicySpec, Policy + +import grid2op +from grid2op.Action import PlayableAction +from grid2op.multi_agent.multiAgentEnv import MultiAgentEnv +from grid2op.gym_compat import GymEnv, BoxGymObsSpace, DiscreteActSpace + +from lightsim2grid import LightSimBackend +from grid2op.multi_agent import ClusterUtils + + + +ENV_NAME = "l2rpn_case14_sandbox" +DO_NOTHING_EPISODES = -1 # 200 + +# Get ACTION_DOMAINS by clustering the substations +ACTION_DOMAINS = ClusterUtils.cluster_substations(ENV_NAME) + +env_for_cls = grid2op.make(ENV_NAME, + action_class=PlayableAction, + backend=LightSimBackend()) +ma_env_for_cls = MultiAgentEnv(env_for_cls, ACTION_DOMAINS) + +# wrapper for gym env +class MAEnvWrapper(MAEnv): + def __init__(self, env_config=None): + super().__init__() + if env_config is None: + env_config = {} + + # you can customize stuff by using the "env config" if you want + backend = LightSimBackend() + if "backend_cls" in env_config: + backend = env_config["backend_cls"] + # you can do the same for other attribute to the environment + + env = grid2op.make(ENV_NAME, + action_class=PlayableAction, + backend=backend) + + + self.ma_env = MultiAgentEnv(env, ACTION_DOMAINS) + self._agent_ids = set(self.ma_env.agents) + self.ma_env.seed(0) + self._agent_ids = self.ma_env.agents + + # see the grid2op doc on how to customize the observation space + # with the grid2op / gym interface. + self._gym_env = GymEnv(env) + self._gym_env.observation_space.close() + + obs_attr_to_keep = ["gen_p", "rho"] + if "obs_attr_to_keep" in env_config: + obs_attr_to_keep = copy.deepcopy(env_config["obs_attr_to_keep"]) + self._gym_env.observation_space = BoxGymObsSpace(env.observation_space, + attr_to_keep=obs_attr_to_keep, + replace_nan_by_0=True # replace Nan by 0. + ) + + # we did not experiment yet with the "partially observable" setting + # so for now we suppose all agents see the same observation + # which is the full grid + self._aux_observation_space = { + agent_id : BoxGymObsSpace(self.ma_env.observation_spaces[agent_id], + attr_to_keep=obs_attr_to_keep, + replace_nan_by_0=True # replace Nan by 0. + ) + for agent_id in self.ma_env.agents + } + # to avoid "weird" pickle issues + self.observation_space = { + agent_id : Box(low=self._aux_observation_space[agent_id].low, + high=self._aux_observation_space[agent_id].high, + dtype=self._aux_observation_space[agent_id].dtype) + for agent_id in self.ma_env.agents + } + + # we represent the action as discrete action for now. + # It should work to encode then differently using the + # gym_compat module for example + act_type = "discrete" + if "act_type" in env_config: + act_type = env_config["act_type"] + + # for discrete actions + if act_type == "discrete": + self._conv_action_space = { + agent_id : DiscreteActSpace(self.ma_env.action_spaces[agent_id]) + for agent_id in self.ma_env.agents + } + + # to avoid "weird" pickle issues + self.action_space = { + agent_id : Discrete(n=self.ma_env.action_spaces[agent_id].n) + for agent_id in self.ma_env.agents + } + else: + raise NotImplementedError("Make the implementation in this case") + + def reset(self, *, seed=None, options=None): + if seed is not None: + self.seed(seed) + + # reset the underlying multi agent environment + obs = self.ma_env.reset() + + return self._format_obs(obs), {} + + def seed(self, seed): + return self.ma_env.seed(seed) + + def _format_obs(self, grid2op_obs): + # NB we heavily use here that all agents see the same things + # grid2op_obs is a dictionnary, representing a "multi agent grid2op action" + + # convert the observation to a gym one (remember we suppose all agents see + # all the grid) + gym_obs = self._gym_env.observation_space.to_gym(grid2op_obs[next(iter(self.ma_env.agents))]) + + # return the proper dictionnary + return { + agent_id : gym_obs.copy() + for agent_id in self.ma_env.agents + } + + def step(self, actions): + # convert the action to grid2op + if actions: + grid2op_act = { + agent_id : self._conv_action_space[agent_id].from_gym(actions[agent_id]) + for agent_id in self.ma_env.agents + } + else: + grid2op_act = { + agent_id : self._conv_action_space[agent_id].from_gym(0) + for agent_id in self.ma_env.agents + } + + # just to retrieve the first agent id... + first_agent_id = next(iter(self.ma_env.agents)) + + # do a step in the underlying multi agent environment + obs, r, done, info = self.ma_env.step(grid2op_act) + + # all agents have the same flag "done" + done['__all__'] = done[first_agent_id] + + # now retrieve the observation in the proper form + gym_obs = self._format_obs(obs) + + # ignored for now + info = {} + truncateds = {k: False for k in self.ma_env.agents} + truncateds['__all__'] = truncateds[first_agent_id] + return gym_obs, r, done, truncateds, info + + +def policy_mapping_fn(agent_id, episode, worker, **kwargs): + return agent_id + + +if __name__ == "__main__": + import ray + # from ray.rllib.agents.ppo import ppo + from ray.rllib.algorithms.ppo import PPO, PPOConfig + import json + import os + import shutil + + ray_ma_env = MAEnvWrapper() + + checkpoint_root = "./ma_ppo_test" + + # Where checkpoints are written: + shutil.rmtree(checkpoint_root, ignore_errors=True, onerror=None) + + # Where some data will be written and used by Tensorboard below: + ray_results = f'{os.getenv("HOME")}/ray_results/' + shutil.rmtree(ray_results, ignore_errors=True, onerror=None) + + info = ray.init(ignore_reinit_error=True) + print("Dashboard URL: http://{}".format(info.address_info["webui_url"])) + + #Configs (see ray's doc for more information) + SELECT_ENV = MAEnvWrapper # Specifies the OpenAI Gym environment for Cart Pole + N_ITER = 1000 # Number of training runs. + + # config = ppo.DEFAULT_CONFIG.copy() # PPO's default configuration. See the next code cell. + # config["log_level"] = "WARN" # Suppress too many messages, but try "INFO" to see what can be printed. + + # # Other settings we might adjust: + # config["num_workers"] = 1 # Use > 1 for using more CPU cores, including over a cluster + # config["num_sgd_iter"] = 10 # Number of SGD (stochastic gradient descent) iterations per training minibatch. + # # I.e., for each minibatch of data, do this many passes over it to train. + # config["sgd_minibatch_size"] = 64 # The amount of data records per minibatch + # config["model"]["fcnet_hiddens"] = [100, 50] # + # config["num_cpus_per_worker"] = 0 # This avoids running out of resources in the notebook environment when this cell is re-executed + # config["vf_clip_param"] = 100 + + # # multi agent specific config + # config["multiagent"] = { + # "policies" : { + # "agent_0" : PolicySpec( + # action_space=ray_ma_env.action_space["agent_0"] + # ), + # "agent_1" : PolicySpec( + # action_space=ray_ma_env.action_space["agent_1"] + # ) + # }, + # "policy_mapping_fn": policy_mapping_fn, + # "policies_to_train": ["agent_0", "agent_1"], + # } + + # see ray doc for this... + # syntax changes every ray major version apparently... + config = PPOConfig() + config = config.training(gamma=0.9, lr=0.01, kl_coeff=0.3, train_batch_size=128) + config = config.resources(num_gpus=0) + config = config.rollouts(num_rollout_workers=1) + + # multi agent parts + config.multi_agent(policies={ + "agent_0" : PolicySpec( + action_space=ray_ma_env.action_space["agent_0"], + observation_space=ray_ma_env.observation_space["agent_0"] + ), + "agent_1" : PolicySpec( + action_space=ray_ma_env.action_space["agent_1"], + observation_space=ray_ma_env.observation_space["agent_1"], + ) + }, + policy_mapping_fn = policy_mapping_fn, + policies_to_train= ["agent_0", "agent_1"]) + + #Trainer + agent = PPO(config=config, env=SELECT_ENV) + + results = [] + episode_data = [] + episode_json = [] + + for n in range(N_ITER): + result = agent.train() + results.append(result) + + episode = {'n': n, + 'episode_reward_min': result['episode_reward_min'], + 'episode_reward_mean': result['episode_reward_mean'], + 'episode_reward_max': result['episode_reward_max'], + 'episode_len_mean': result['episode_len_mean'] + } + + episode_data.append(episode) + episode_json.append(json.dumps(episode)) + file_name = agent.save(checkpoint_root) + + print(f'{n:3d}: Min/Mean/Max reward: {result["episode_reward_min"]:8.4f}/{result["episode_reward_mean"]:8.4f}/{result["episode_reward_max"]:8.4f}. Checkpoint saved to {file_name}') + + with open(f'{ray_results}/rewards.json', 'w') as outfile: + json.dump(episode_json, outfile) diff --git a/grid2op/multi_agent/__init__.py b/grid2op/multi_agent/__init__.py index be787cd43..b438db11e 100644 --- a/grid2op/multi_agent/__init__.py +++ b/grid2op/multi_agent/__init__.py @@ -9,7 +9,8 @@ __all__ = ["SubGridAction", "SubGridObservation", "MultiAgentEnv", - "SubGridObjects"] + "SubGridObjects", + "ClusterUtils"] import warnings from grid2op.multi_agent.ma_exceptions import MultiAgentStillBeta @@ -25,3 +26,4 @@ from grid2op.multi_agent.subgridObservation import SubGridObservation from grid2op.multi_agent.multiAgentEnv import MultiAgentEnv from grid2op.multi_agent.subGridObjects import SubGridObjects +from grid2op.multi_agent.utils import ClusterUtils \ No newline at end of file diff --git a/grid2op/multi_agent/utils.py b/grid2op/multi_agent/utils.py index e6dcfb2eb..fad5c76ad 100644 --- a/grid2op/multi_agent/utils.py +++ b/grid2op/multi_agent/utils.py @@ -7,7 +7,11 @@ # This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. from numpy.random import shuffle - +import grid2op +from lightsim2grid.lightSimBackend import LightSimBackend +import numpy as np +from sknetwork.clustering import Louvain +from scipy.sparse import csr_matrix def random_order(agents : list, *args, **kwargs): """Returns the random order @@ -74,4 +78,74 @@ def __eq__(self, other): and self._current_agent == other._current_agent and self.selected_agent == other.selected_agent ) - \ No newline at end of file + + +class ClusterUtils: + """ + Utility class for clustering substations + """ + + # Create connectivity matrix + @staticmethod + def create_connectivity_matrix(env): + """ + Creates a connectivity matrix for the given grid environment. + + The connectivity matrix is a 2D NumPy array where the element at position (i, j) is 1 if there is a direct + connection between substation i and substation j, and 0 otherwise. The diagonal elements are set to 1 to indicate + self-connections. + + Args: + env (grid2op.Environment): The grid environment for which the connectivity matrix is to be created. + + Returns: + np.ndarray(env.n_sub, env.n_sub): A 2D NumPy array representing the connectivity matrix of the grid environment. + """ + connectivity_matrix = np.zeros((env.n_sub, env.n_sub)) + for line_id in range(env.n_line): + orig_sub = env.line_or_to_subid[line_id] + extrem_sub = env.line_ex_to_subid[line_id] + connectivity_matrix[orig_sub, extrem_sub] = 1 + connectivity_matrix[extrem_sub, orig_sub] = 1 + return connectivity_matrix + np.eye(env.n_sub) + + + + # Cluster substations + def cluster_substations(env_name): + """ + Clusters substations in a power grid environment using the Louvain community detection algorithm. + + This function creates a grid environment based on the specified environment name, generates a connectivity matrix + representing the connections between substations, and applies the Louvain algorithm to cluster the substations + into communities. The resulting clusters are formatted into a dictionary where each key corresponds to an agent + and the value is a list of substations assigned to that agent. + + Args: + env_name (str): The name of the grid environment to be clustered + + Returns: + (MADict): + - keys : agents' names + - values : list of substations' id under the control of the agent. + """ + # Create the environment + env = grid2op.make(env_name, backend=LightSimBackend()) + + # Generate the connectivity matrix + matrix = ClusterUtils.create_connectivity_matrix(env) + + # Perform clustering using Louvain algorithm + louvain = Louvain() + adjacency = csr_matrix(matrix) + labels = louvain.fit_predict(adjacency) + + # Group substations into clusters + clusters = {} + for node, label in enumerate(labels): + if label not in clusters: + clusters[label] = [] + clusters[label].append(node) + + # Format the clusters + formatted_clusters = {f'agent_{i}': nodes for i, nodes in enumerate(clusters.values())} \ No newline at end of file diff --git a/setup.py b/setup.py index 976d4ff15..63cb1d723 100644 --- a/setup.py +++ b/setup.py @@ -30,6 +30,9 @@ def my_test_suite(): "tqdm>=4.45.0", "networkx>=2.4", "requests>=2.23.0", + "scikit-network>=0.32.1", + "lightsim2grid>=0.8.2", + "numpy>=1.25.2", "packaging", # because gym changes the way it uses numpy prng in version 0.26 and i need both gym before and after... "typing_extensions" ], From e4a4172c1d12fdd6b30e3e6855b7489dba3b35bf Mon Sep 17 00:00:00 2001 From: BamunugeDR99 Date: Mon, 3 Jun 2024 09:46:03 +0530 Subject: [PATCH 02/11] fix: Added missing return to cluster_substations function --- grid2op/multi_agent/utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/grid2op/multi_agent/utils.py b/grid2op/multi_agent/utils.py index fad5c76ad..f8f9d2ca2 100644 --- a/grid2op/multi_agent/utils.py +++ b/grid2op/multi_agent/utils.py @@ -148,4 +148,6 @@ def cluster_substations(env_name): clusters[label].append(node) # Format the clusters - formatted_clusters = {f'agent_{i}': nodes for i, nodes in enumerate(clusters.values())} \ No newline at end of file + formatted_clusters = {f'agent_{i}': nodes for i, nodes in enumerate(clusters.values())} + + return formatted_clusters \ No newline at end of file From 73d5b09c168abddb00579f539334a3359c0b3524 Mon Sep 17 00:00:00 2001 From: BamunugeDR99 Date: Mon, 3 Jun 2024 12:02:27 +0530 Subject: [PATCH 03/11] fix: Converted cluster_substations in to a static method --- grid2op/multi_agent/utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/grid2op/multi_agent/utils.py b/grid2op/multi_agent/utils.py index f8f9d2ca2..7e957549e 100644 --- a/grid2op/multi_agent/utils.py +++ b/grid2op/multi_agent/utils.py @@ -110,8 +110,9 @@ def create_connectivity_matrix(env): return connectivity_matrix + np.eye(env.n_sub) - + # Cluster substations + @staticmethod def cluster_substations(env_name): """ Clusters substations in a power grid environment using the Louvain community detection algorithm. From 088de74c4d630e9bc9d3670bdb6ceb8c5d83584f Mon Sep 17 00:00:00 2001 From: BamunugeDR99 Date: Tue, 4 Jun 2024 12:21:11 +0530 Subject: [PATCH 04/11] feat: Retrieved OBSERVATION_DOMAINS through substation clustering --- examples/multi_agents/ray_example3.py | 85 ++++++++++++++------------- 1 file changed, 44 insertions(+), 41 deletions(-) diff --git a/examples/multi_agents/ray_example3.py b/examples/multi_agents/ray_example3.py index 4baf8f9db..885a45a34 100644 --- a/examples/multi_agents/ray_example3.py +++ b/examples/multi_agents/ray_example3.py @@ -6,7 +6,8 @@ # SPDX-License-Identifier: MPL-2.0 # This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. -"""example with centralized observation and local actions""" +"""example with local observation and local actions""" + import warnings import numpy as np import copy @@ -22,20 +23,21 @@ from grid2op.gym_compat import GymEnv, BoxGymObsSpace, DiscreteActSpace from lightsim2grid import LightSimBackend +from grid2op.gym_compat.utils import ALL_ATTR_FOR_DISCRETE from grid2op.multi_agent import ClusterUtils - - ENV_NAME = "l2rpn_case14_sandbox" DO_NOTHING_EPISODES = -1 # 200 # Get ACTION_DOMAINS by clustering the substations ACTION_DOMAINS = ClusterUtils.cluster_substations(ENV_NAME) + +# Get OBSERVATION_DOMAINS by clustering the substations +OBSERVATION_DOMAINS = ClusterUtils.cluster_substations(ENV_NAME) env_for_cls = grid2op.make(ENV_NAME, action_class=PlayableAction, backend=LightSimBackend()) -ma_env_for_cls = MultiAgentEnv(env_for_cls, ACTION_DOMAINS) # wrapper for gym env class MAEnvWrapper(MAEnv): @@ -44,18 +46,20 @@ def __init__(self, env_config=None): if env_config is None: env_config = {} - # you can customize stuff by using the "env config" if you want - backend = LightSimBackend() - if "backend_cls" in env_config: - backend = env_config["backend_cls"] - # you can do the same for other attribute to the environment - env = grid2op.make(ENV_NAME, action_class=PlayableAction, - backend=backend) - + backend=LightSimBackend()) - self.ma_env = MultiAgentEnv(env, ACTION_DOMAINS) + action_domains = copy.deepcopy(ACTION_DOMAINS) + if "action_domains" in env_config: + action_domains = env_config["action_domains"] + observation_domains = copy.deepcopy(OBSERVATION_DOMAINS) + if "observation_domains" in env_config: + observation_domains = env_config["observation_domains"] + self.ma_env = MultiAgentEnv(env, + action_domains, + observation_domains) + self._agent_ids = set(self.ma_env.agents) self.ma_env.seed(0) self._agent_ids = self.ma_env.agents @@ -64,7 +68,6 @@ def __init__(self, env_config=None): # with the grid2op / gym interface. self._gym_env = GymEnv(env) self._gym_env.observation_space.close() - obs_attr_to_keep = ["gen_p", "rho"] if "obs_attr_to_keep" in env_config: obs_attr_to_keep = copy.deepcopy(env_config["obs_attr_to_keep"]) @@ -90,28 +93,24 @@ def __init__(self, env_config=None): dtype=self._aux_observation_space[agent_id].dtype) for agent_id in self.ma_env.agents } - + # we represent the action as discrete action for now. # It should work to encode then differently using the # gym_compat module for example - act_type = "discrete" - if "act_type" in env_config: - act_type = env_config["act_type"] - - # for discrete actions - if act_type == "discrete": - self._conv_action_space = { - agent_id : DiscreteActSpace(self.ma_env.action_spaces[agent_id]) - for agent_id in self.ma_env.agents - } + act_attr_to_keep = ALL_ATTR_FOR_DISCRETE + if "act_attr_to_keep" in env_config: + act_attr_to_keep = copy.deepcopy(env_config["act_attr_to_keep"]) - # to avoid "weird" pickle issues - self.action_space = { - agent_id : Discrete(n=self.ma_env.action_spaces[agent_id].n) - for agent_id in self.ma_env.agents - } - else: - raise NotImplementedError("Make the implementation in this case") + self._conv_action_space = { + agent_id : DiscreteActSpace(self.ma_env.action_spaces[agent_id], attr_to_keep=act_attr_to_keep) + for agent_id in self.ma_env.agents + } + + # to avoid "weird" pickle issues + self.action_space = { + agent_id : Discrete(n=self.ma_env.action_spaces[agent_id].n) + for agent_id in self.ma_env.agents + } def reset(self, *, seed=None, options=None): if seed is not None: @@ -124,18 +123,16 @@ def reset(self, *, seed=None, options=None): def seed(self, seed): return self.ma_env.seed(seed) + def _format_obs(self, grid2op_obs): - # NB we heavily use here that all agents see the same things # grid2op_obs is a dictionnary, representing a "multi agent grid2op action" - # convert the observation to a gym one (remember we suppose all agents see - # all the grid) - gym_obs = self._gym_env.observation_space.to_gym(grid2op_obs[next(iter(self.ma_env.agents))]) + # convert the observation to a gym one # return the proper dictionnary return { - agent_id : gym_obs.copy() + agent_id : self._aux_observation_space[agent_id].to_gym(grid2op_obs[agent_id]) for agent_id in self.ma_env.agents } @@ -185,7 +182,7 @@ def policy_mapping_fn(agent_id, episode, worker, **kwargs): ray_ma_env = MAEnvWrapper() - checkpoint_root = "./ma_ppo_test" + checkpoint_root = "./ma_ppo_test_2ndsetting" # Where checkpoints are written: shutil.rmtree(checkpoint_root, ignore_errors=True, onerror=None) @@ -197,7 +194,7 @@ def policy_mapping_fn(agent_id, episode, worker, **kwargs): info = ray.init(ignore_reinit_error=True) print("Dashboard URL: http://{}".format(info.address_info["webui_url"])) - #Configs (see ray's doc for more information) + # #Configs (see ray's doc for more information) SELECT_ENV = MAEnvWrapper # Specifies the OpenAI Gym environment for Cart Pole N_ITER = 1000 # Number of training runs. @@ -217,15 +214,21 @@ def policy_mapping_fn(agent_id, episode, worker, **kwargs): # config["multiagent"] = { # "policies" : { # "agent_0" : PolicySpec( - # action_space=ray_ma_env.action_space["agent_0"] + # action_space=ray_ma_env.action_space["agent_0"], + # observation_space=ray_ma_env.observation_space["agent_0"], # ), # "agent_1" : PolicySpec( - # action_space=ray_ma_env.action_space["agent_1"] + # action_space=ray_ma_env.action_space["agent_1"], + # observation_space=ray_ma_env.observation_space["agent_1"], # ) # }, # "policy_mapping_fn": policy_mapping_fn, # "policies_to_train": ["agent_0", "agent_1"], # } + + # #Trainer + # agent = ppo.PPOTrainer(config, env=SELECT_ENV) + # see ray doc for this... # syntax changes every ray major version apparently... From e4f51c0971402b0ec79108ac4cd8c2c22e0d01cb Mon Sep 17 00:00:00 2001 From: BamunugeDR99 Date: Tue, 4 Jun 2024 12:23:39 +0530 Subject: [PATCH 05/11] fix: Updated the docstrings --- grid2op/multi_agent/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/grid2op/multi_agent/utils.py b/grid2op/multi_agent/utils.py index 7e957549e..9d553e467 100644 --- a/grid2op/multi_agent/utils.py +++ b/grid2op/multi_agent/utils.py @@ -82,7 +82,7 @@ def __eq__(self, other): class ClusterUtils: """ - Utility class for clustering substations + Outputs clustered substation based on the Louvain graph clustering method """ # Create connectivity matrix @@ -99,7 +99,7 @@ def create_connectivity_matrix(env): env (grid2op.Environment): The grid environment for which the connectivity matrix is to be created. Returns: - np.ndarray(env.n_sub, env.n_sub): A 2D NumPy array representing the connectivity matrix of the grid environment. + connectivity_matrix: A 2D Numpy array of dimension (env.n_sub, env.n_sub) representing the substation connectivity of the grid environment. """ connectivity_matrix = np.zeros((env.n_sub, env.n_sub)) for line_id in range(env.n_line): From 9c7ce1b430e4353b93e0853bb2291dac4401ad4e Mon Sep 17 00:00:00 2001 From: BamunugeDR99 Date: Tue, 4 Jun 2024 13:54:15 +0530 Subject: [PATCH 06/11] fix: Removed environment creation from the util function --- examples/multi_agents/ray_example3.py | 13 +++++++------ grid2op/multi_agent/utils.py | 20 ++++++++------------ 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/examples/multi_agents/ray_example3.py b/examples/multi_agents/ray_example3.py index 885a45a34..91815a2f3 100644 --- a/examples/multi_agents/ray_example3.py +++ b/examples/multi_agents/ray_example3.py @@ -29,16 +29,17 @@ ENV_NAME = "l2rpn_case14_sandbox" DO_NOTHING_EPISODES = -1 # 200 -# Get ACTION_DOMAINS by clustering the substations -ACTION_DOMAINS = ClusterUtils.cluster_substations(ENV_NAME) - -# Get OBSERVATION_DOMAINS by clustering the substations -OBSERVATION_DOMAINS = ClusterUtils.cluster_substations(ENV_NAME) - env_for_cls = grid2op.make(ENV_NAME, action_class=PlayableAction, backend=LightSimBackend()) + +# Get ACTION_DOMAINS by clustering the substations +ACTION_DOMAINS = ClusterUtils.cluster_substations(env_for_cls) + +# Get OBSERVATION_DOMAINS by clustering the substations +OBSERVATION_DOMAINS = ClusterUtils.cluster_substations(env_for_cls) + # wrapper for gym env class MAEnvWrapper(MAEnv): def __init__(self, env_config=None): diff --git a/grid2op/multi_agent/utils.py b/grid2op/multi_agent/utils.py index 9d553e467..fef758ebe 100644 --- a/grid2op/multi_agent/utils.py +++ b/grid2op/multi_agent/utils.py @@ -6,9 +6,8 @@ # SPDX-License-Identifier: MPL-2.0 # This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. +from grid2op.Environment import Environment from numpy.random import shuffle -import grid2op -from lightsim2grid.lightSimBackend import LightSimBackend import numpy as np from sknetwork.clustering import Louvain from scipy.sparse import csr_matrix @@ -82,12 +81,12 @@ def __eq__(self, other): class ClusterUtils: """ - Outputs clustered substation based on the Louvain graph clustering method + Outputs clustered substation based on the Louvain graph clustering method. """ # Create connectivity matrix @staticmethod - def create_connectivity_matrix(env): + def create_connectivity_matrix(env:Environment): """ Creates a connectivity matrix for the given grid environment. @@ -113,25 +112,22 @@ def create_connectivity_matrix(env): # Cluster substations @staticmethod - def cluster_substations(env_name): + def cluster_substations(env:Environment): """ Clusters substations in a power grid environment using the Louvain community detection algorithm. - This function creates a grid environment based on the specified environment name, generates a connectivity matrix - representing the connections between substations, and applies the Louvain algorithm to cluster the substations - into communities. The resulting clusters are formatted into a dictionary where each key corresponds to an agent - and the value is a list of substations assigned to that agent. + This function generates a connectivity matrix representing the connections between substations in the given environment, + and applies the Louvain algorithm to cluster the substations into communities. The resulting clusters are formatted into + a dictionary where each key corresponds to an agent and the value is a list of substations assigned to that agent. Args: - env_name (str): The name of the grid environment to be clustered + env (grid2op.Environment): The grid environment for which the connectivity matrix is to be created. Returns: (MADict): - keys : agents' names - values : list of substations' id under the control of the agent. """ - # Create the environment - env = grid2op.make(env_name, backend=LightSimBackend()) # Generate the connectivity matrix matrix = ClusterUtils.create_connectivity_matrix(env) From a39c38788e0431d000cc2e62aaa9d178fdd6b76b Mon Sep 17 00:00:00 2001 From: BamunugeDR99 Date: Thu, 6 Jun 2024 12:31:55 +0530 Subject: [PATCH 07/11] fix: Removed the light2simgrid library --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index 63cb1d723..d4369ba4c 100644 --- a/setup.py +++ b/setup.py @@ -31,7 +31,6 @@ def my_test_suite(): "networkx>=2.4", "requests>=2.23.0", "scikit-network>=0.32.1", - "lightsim2grid>=0.8.2", "numpy>=1.25.2", "packaging", # because gym changes the way it uses numpy prng in version 0.26 and i need both gym before and after... "typing_extensions" From 9296d13d3a1fa8c875b98ecad90957aea0aa2416 Mon Sep 17 00:00:00 2001 From: BamunugeDR99 Date: Thu, 27 Jun 2024 09:46:15 +0530 Subject: [PATCH 08/11] fix: Removed conflicting numpy library --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index d4369ba4c..f003e8dda 100644 --- a/setup.py +++ b/setup.py @@ -31,7 +31,6 @@ def my_test_suite(): "networkx>=2.4", "requests>=2.23.0", "scikit-network>=0.32.1", - "numpy>=1.25.2", "packaging", # because gym changes the way it uses numpy prng in version 0.26 and i need both gym before and after... "typing_extensions" ], From efe5d052d13bec34eda5cee0becd61e094a2e1e7 Mon Sep 17 00:00:00 2001 From: BamunugeDR99 Date: Wed, 13 Nov 2024 10:22:34 +0530 Subject: [PATCH 09/11] fix : PR changes -> Moved and renamed the ClusterUtils --- examples/multi_agents/ray_example3.py | 6 +- grid2op/cluster_utils/louvainClustering.py | 74 +++++++++++++++++++++ grid2op/multi_agent/__init__.py | 4 +- grid2op/multi_agent/utils.py | 75 ---------------------- setup.py | 2 +- 5 files changed, 80 insertions(+), 81 deletions(-) create mode 100644 grid2op/cluster_utils/louvainClustering.py diff --git a/examples/multi_agents/ray_example3.py b/examples/multi_agents/ray_example3.py index 91815a2f3..17e89fb4e 100644 --- a/examples/multi_agents/ray_example3.py +++ b/examples/multi_agents/ray_example3.py @@ -24,7 +24,7 @@ from lightsim2grid import LightSimBackend from grid2op.gym_compat.utils import ALL_ATTR_FOR_DISCRETE -from grid2op.multi_agent import ClusterUtils +from grid2op.multi_agent import LouvainClustering ENV_NAME = "l2rpn_case14_sandbox" DO_NOTHING_EPISODES = -1 # 200 @@ -35,10 +35,10 @@ # Get ACTION_DOMAINS by clustering the substations -ACTION_DOMAINS = ClusterUtils.cluster_substations(env_for_cls) +ACTION_DOMAINS = LouvainClustering.cluster_substations(env_for_cls) # Get OBSERVATION_DOMAINS by clustering the substations -OBSERVATION_DOMAINS = ClusterUtils.cluster_substations(env_for_cls) +OBSERVATION_DOMAINS = LouvainClustering.cluster_substations(env_for_cls) # wrapper for gym env class MAEnvWrapper(MAEnv): diff --git a/grid2op/cluster_utils/louvainClustering.py b/grid2op/cluster_utils/louvainClustering.py new file mode 100644 index 000000000..c1983c165 --- /dev/null +++ b/grid2op/cluster_utils/louvainClustering.py @@ -0,0 +1,74 @@ +from grid2op.Environment import Environment +import numpy as np +from sknetwork.clustering import Louvain +from scipy.sparse import csr_matrix + +class LouvainClustering: + """ + Outputs clustered substation based on the Louvain graph clustering method. + """ + + # Create connectivity matrix + @staticmethod + def create_connectivity_matrix(env:Environment): + """ + Creates a connectivity matrix for the given grid environment. + + The connectivity matrix is a 2D NumPy array where the element at position (i, j) is 1 if there is a direct + connection between substation i and substation j, and 0 otherwise. The diagonal elements are set to 1 to indicate + self-connections. + + Args: + env (grid2op.Environment): The grid environment for which the connectivity matrix is to be created. + + Returns: + connectivity_matrix: A 2D Numpy array of dimension (env.n_sub, env.n_sub) representing the substation connectivity of the grid environment. + """ + connectivity_matrix = np.zeros((env.n_sub, env.n_sub)) + for line_id in range(env.n_line): + orig_sub = env.line_or_to_subid[line_id] + extrem_sub = env.line_ex_to_subid[line_id] + connectivity_matrix[orig_sub, extrem_sub] = 1 + connectivity_matrix[extrem_sub, orig_sub] = 1 + return connectivity_matrix + np.eye(env.n_sub) + + + + # Cluster substations + @staticmethod + def cluster_substations(env:Environment): + """ + Clusters substations in a power grid environment using the Louvain community detection algorithm. + + This function generates a connectivity matrix representing the connections between substations in the given environment, + and applies the Louvain algorithm to cluster the substations into communities. The resulting clusters are formatted into + a dictionary where each key corresponds to an agent and the value is a list of substations assigned to that agent. + + Args: + env (grid2op.Environment): The grid environment for which the connectivity matrix is to be created. + + Returns: + (MADict): + - keys : agents' names + - values : list of substations' id under the control of the agent. + """ + + # Generate the connectivity matrix + matrix = LouvainClustering.create_connectivity_matrix(env) + + # Perform clustering using Louvain algorithm + louvain = Louvain() + adjacency = csr_matrix(matrix) + labels = louvain.fit_predict(adjacency) + + # Group substations into clusters + clusters = {} + for node, label in enumerate(labels): + if label not in clusters: + clusters[label] = [] + clusters[label].append(node) + + # Format the clusters + formatted_clusters = {f'agent_{i}': nodes for i, nodes in enumerate(clusters.values())} + + return formatted_clusters \ No newline at end of file diff --git a/grid2op/multi_agent/__init__.py b/grid2op/multi_agent/__init__.py index b438db11e..52e726b8f 100644 --- a/grid2op/multi_agent/__init__.py +++ b/grid2op/multi_agent/__init__.py @@ -10,7 +10,7 @@ "SubGridObservation", "MultiAgentEnv", "SubGridObjects", - "ClusterUtils"] + "LouvainClustering"] import warnings from grid2op.multi_agent.ma_exceptions import MultiAgentStillBeta @@ -26,4 +26,4 @@ from grid2op.multi_agent.subgridObservation import SubGridObservation from grid2op.multi_agent.multiAgentEnv import MultiAgentEnv from grid2op.multi_agent.subGridObjects import SubGridObjects -from grid2op.multi_agent.utils import ClusterUtils \ No newline at end of file +from grid2op.cluster_utils.louvainClustering import LouvainClustering \ No newline at end of file diff --git a/grid2op/multi_agent/utils.py b/grid2op/multi_agent/utils.py index fef758ebe..6edca05ce 100644 --- a/grid2op/multi_agent/utils.py +++ b/grid2op/multi_agent/utils.py @@ -6,11 +6,7 @@ # SPDX-License-Identifier: MPL-2.0 # This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. -from grid2op.Environment import Environment from numpy.random import shuffle -import numpy as np -from sknetwork.clustering import Louvain -from scipy.sparse import csr_matrix def random_order(agents : list, *args, **kwargs): """Returns the random order @@ -77,74 +73,3 @@ def __eq__(self, other): and self._current_agent == other._current_agent and self.selected_agent == other.selected_agent ) - - -class ClusterUtils: - """ - Outputs clustered substation based on the Louvain graph clustering method. - """ - - # Create connectivity matrix - @staticmethod - def create_connectivity_matrix(env:Environment): - """ - Creates a connectivity matrix for the given grid environment. - - The connectivity matrix is a 2D NumPy array where the element at position (i, j) is 1 if there is a direct - connection between substation i and substation j, and 0 otherwise. The diagonal elements are set to 1 to indicate - self-connections. - - Args: - env (grid2op.Environment): The grid environment for which the connectivity matrix is to be created. - - Returns: - connectivity_matrix: A 2D Numpy array of dimension (env.n_sub, env.n_sub) representing the substation connectivity of the grid environment. - """ - connectivity_matrix = np.zeros((env.n_sub, env.n_sub)) - for line_id in range(env.n_line): - orig_sub = env.line_or_to_subid[line_id] - extrem_sub = env.line_ex_to_subid[line_id] - connectivity_matrix[orig_sub, extrem_sub] = 1 - connectivity_matrix[extrem_sub, orig_sub] = 1 - return connectivity_matrix + np.eye(env.n_sub) - - - - # Cluster substations - @staticmethod - def cluster_substations(env:Environment): - """ - Clusters substations in a power grid environment using the Louvain community detection algorithm. - - This function generates a connectivity matrix representing the connections between substations in the given environment, - and applies the Louvain algorithm to cluster the substations into communities. The resulting clusters are formatted into - a dictionary where each key corresponds to an agent and the value is a list of substations assigned to that agent. - - Args: - env (grid2op.Environment): The grid environment for which the connectivity matrix is to be created. - - Returns: - (MADict): - - keys : agents' names - - values : list of substations' id under the control of the agent. - """ - - # Generate the connectivity matrix - matrix = ClusterUtils.create_connectivity_matrix(env) - - # Perform clustering using Louvain algorithm - louvain = Louvain() - adjacency = csr_matrix(matrix) - labels = louvain.fit_predict(adjacency) - - # Group substations into clusters - clusters = {} - for node, label in enumerate(labels): - if label not in clusters: - clusters[label] = [] - clusters[label].append(node) - - # Format the clusters - formatted_clusters = {f'agent_{i}': nodes for i, nodes in enumerate(clusters.values())} - - return formatted_clusters \ No newline at end of file diff --git a/setup.py b/setup.py index a08bb9fa3..a420503ea 100644 --- a/setup.py +++ b/setup.py @@ -30,7 +30,6 @@ def my_test_suite(): "tqdm>=4.45.0", "networkx>=2.4", "requests>=2.23.0", - "scikit-network>=0.32.1", "packaging", # because gym changes the way it uses numpy prng in version 0.26 and i need both gym before and after... "typing_extensions" ], @@ -42,6 +41,7 @@ def my_test_suite(): "numba>=0.48.0", "matplotlib>=3.2.1", "plotly>=4.5.4", + "scikit-network>=0.32.1", "seaborn>=0.10.0", "imageio>=2.8.0", "pygifsicle>=1.0.1", From da99bb9a8459cecd6332bd46d1ecabdf54b25687 Mon Sep 17 00:00:00 2001 From: BamunugeDR99 Date: Tue, 19 Nov 2024 09:26:42 +0530 Subject: [PATCH 10/11] feat: Added Test Cases and the names of the authors --- AUTHORS.txt | 4 +- grid2op/tests/test_Louvain_Clustering.py | 73 ++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 grid2op/tests/test_Louvain_Clustering.py diff --git a/AUTHORS.txt b/AUTHORS.txt index c08c8d1f3..b49af130c 100644 --- a/AUTHORS.txt +++ b/AUTHORS.txt @@ -17,4 +17,6 @@ Further Contributions by: - Vincent Renault - Florian Schäfer - Clément Goubet - - Laure Crochepierre \ No newline at end of file + - Laure Crochepierre + - Dulan Bamunuge (Louvain Clustering) + - Thirunayan Dinesh (Louvain Clustering) \ No newline at end of file diff --git a/grid2op/tests/test_Louvain_Clustering.py b/grid2op/tests/test_Louvain_Clustering.py new file mode 100644 index 000000000..452c326a9 --- /dev/null +++ b/grid2op/tests/test_Louvain_Clustering.py @@ -0,0 +1,73 @@ +import unittest +import numpy as np +from cluster_utils.louvainClustering import LouvainClustering +import grid2op +from lightsim2grid.lightSimBackend import LightSimBackend + +class TestLouvainClustering(unittest.TestCase): + + def test_create_connectivity_matrix(self): + """ + Test the creation of the connectivity matrix + """ + env_name = "l2rpn_case14_sandbox" + env = grid2op.make(env_name, backend=LightSimBackend(), test=True) + + # Expected connectivity matrix + expected_matrix = np.array([ + [1., 1., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0.], + [1., 1., 1., 1., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0.], + [0., 1., 1., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], + [0., 1., 1., 1., 1., 0., 1., 0., 1., 0., 0., 0., 0., 0.], + [1., 1., 0., 1., 1., 1., 0., 0., 0., 0., 0., 0., 0., 0.], + [0., 0., 0., 0., 1., 1., 0., 0., 0., 0., 1., 1., 1., 0.], + [0., 0., 0., 1., 0., 0., 1., 1., 1., 0., 0., 0., 0., 0.], + [0., 0., 0., 0., 0., 0., 1., 1., 0., 0., 0., 0., 0., 0.], + [0., 0., 0., 1., 0., 0., 1., 0., 1., 1., 0., 0., 0., 1.], + [0., 0., 0., 0., 0., 0., 0., 0., 1., 1., 1., 0., 0., 0.], + [0., 0., 0., 0., 0., 1., 0., 0., 0., 1., 1., 0., 0., 0.], + [0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 1., 1., 0.], + [0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 1., 1., 1.], + [0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0., 1., 1.] + ]) + + # Generate the connectivity matrix + actual_matrix = LouvainClustering.create_connectivity_matrix(env) + + # Validate the generated matrix + np.testing.assert_array_almost_equal(actual_matrix, expected_matrix, err_msg="Connectivity matrix does not match the expected matrix.") + + print("Test passed for create_connectivity_matrix.") + + + def test_cluster_substations(self): + """ + Test the clustering of substations using the Louvain algorithm + """ + env_name = "l2rpn_case14_sandbox" + env = grid2op.make(env_name, backend=LightSimBackend(), test=True) + + # Expected clustering result + expected_clusters = { + 'agent_0': [0, 1, 2, 3, 4], + 'agent_1': [5, 11, 12], + 'agent_2': [6, 7, 8, 13], + 'agent_3': [9, 10] + } + + # Generate the clustering + actual_clusters = LouvainClustering.cluster_substations(env) + + # Validate the generated clustering + self.assertEqual( + actual_clusters, + expected_clusters, + f"Clustered substations do not match the expected result. Got {actual_clusters}" + ) + + print("Test passed for cluster_substations.") + +if __name__ == '__main__': + unittest.main() + + From 8238d236e335ba15425978a14b186ded10588041 Mon Sep 17 00:00:00 2001 From: BamunugeDR99 Date: Tue, 19 Nov 2024 16:34:38 +0530 Subject: [PATCH 11/11] fix: Author Names Updated --- AUTHORS.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AUTHORS.txt b/AUTHORS.txt index b49af130c..6fcf5e0f9 100644 --- a/AUTHORS.txt +++ b/AUTHORS.txt @@ -18,5 +18,5 @@ Further Contributions by: - Florian Schäfer - Clément Goubet - Laure Crochepierre - - Dulan Bamunuge (Louvain Clustering) - - Thirunayan Dinesh (Louvain Clustering) \ No newline at end of file + - Dulan Bamunuge + - Thirunayan Dinesh \ No newline at end of file