Source code for pyrat.src.mazes.maze_from_matrix
##########################################################################################
########################################## INFO ##########################################
##########################################################################################
# This file is part of the PyRat library.
# It is meant to be used as a library, and not to be executed directly.
# Please import necessary elements using the following syntax:
# from pyrat import <element_name>
"""
This module provides a maze that is created from a fixed description as a ``numpy.ndarray`` or a ``torch.tensor``.
Indices of rows and columns are the indices of the corresponding cells.
This class is especially useful to allow exporting a maze to a file, and then reusing it later.
It is also useful to test a player on a fixed maze.
"""
##########################################################################################
######################################### IMPORTS ########################################
##########################################################################################
# External imports
import math
# PyRat imports
from pyrat.src.mazes.maze import Maze
from pyrat.src.game.exceptions import PyRatException
##########################################################################################
######################################### CLASSES ########################################
##########################################################################################
[docs]
class MazeFromMatrix (Maze):
"""
*(This class inherits from* ``Maze`` *).*
This is a maze that is created from a fixed description as a numpy ndarray or a torch tensor.
Indices of rows and columns are the indices of the corresponding cells.
Entries are the weights of the corresponding edges.
Rows and columns with only 0 values will be ignored.
This class can be useful to test a player on a fixed maze, to compare its performance with other players.
"""
##################################################################################
# CONSTRUCTOR #
##################################################################################
[docs]
def __init__ ( self,
description: object,
*args: object,
**kwargs: object
) -> None:
"""
Initializes a new instance of the class.
Args:
description: Fixed maze as a matrix (not specified for optional dependency reasons).
*args: Arguments to pass to the parent constructor.
**kwargs: Keyword arguments to pass to the parent constructor.
Raises:
PyRatException: If a width or a height is given, as they are inferred from the description.
"""
# The width and height are inferred from the description, so they must not be given
given_dimensions = [f"{key}={kwargs[key]!r}" for key in ("width", "height") if key in kwargs] + [f"{value!r} (positional)" for value in args[:2]]
if given_dimensions:
raise PyRatException(f"Arguments 'width' and 'height' must not be given to MazeFromMatrix, as they are inferred from 'description', got {', '.join(given_dimensions)}")
# Inherit from parent class
super().__init__(*args, **kwargs)
# Check validity
if str(type(description)) not in ["<class 'numpy.ndarray'>", "<class 'torch.Tensor'>"]:
raise PyRatException(f"Argument 'description' must be a numpy.ndarray or a torch.Tensor, got a value of type {type(description).__name__}")
if len(description.shape) != 2:
raise PyRatException(f"Argument 'description' must be a 2D matrix, got an array of shape {tuple(description.shape)}")
if description.shape[0] != description.shape[1]:
raise PyRatException(f"Argument 'description' must be a square matrix, got a matrix of shape {tuple(description.shape)}")
if not (description.shape[0] > 1):
raise PyRatException(f"The maze must have at least two vertices, got a matrix of shape {tuple(description.shape)}")
if str(description.dtype) == "object":
raise PyRatException("Argument 'description' must be a matrix of integers, got a matrix of dtype object (convert it first, for instance with description.astype(int))")
if not all(isinstance(weight, int) and not isinstance(weight, bool) for weight in description.flatten().tolist()):
raise PyRatException(f"All entries of 'description' must be integers, got a matrix of dtype {description.dtype}")
if not (description == description.T).all():
raise PyRatException(f"Argument 'description' must be a symmetric matrix (description[i, j] == description[j, i]), got a non-symmetric matrix of shape {tuple(description.shape)}")
if not (description >= 0).all():
raise PyRatException(f"All entries of 'description' must be non-negative, got a matrix of shape {tuple(description.shape)} with negative entries")
if not (description.diagonal() == 0).all():
raise PyRatException(f"All diagonal entries of 'description' must be 0, as a cell cannot be its own neighbor, got a matrix of shape {tuple(description.shape)} with non-zero diagonal entries")
if not (description > 0).any():
raise PyRatException(f"The maze must have at least one edge, got a matrix of shape {tuple(description.shape)} with no positive entry")
# Private attributes
self.__description = description
# Generate the maze
self._create_maze()
##################################################################################
# PROTECTED METHODS #
##################################################################################
def _create_maze (self) -> None:
"""
*(This method redefines the method of the parent class with the same name).*
Creates a maze from the description provided at initialization.
"""
# Determine the vertices
vertices = []
for vertex in range(self.__description.shape[0]):
neighbors = [neighbor for neighbor in range(self.__description.shape[1]) if self.__description[vertex, neighbor] > 0]
if len(neighbors) > 0:
vertices.append(vertex)
# Determine the edges
edges = []
for vertex in range(self.__description.shape[0]):
for neighbor in range(self.__description.shape[1]):
if self.__description[vertex, neighbor] > 0:
edges.append((vertex, neighbor, self.__description[vertex, neighbor].item()))
# Determine the dimensions of the maze
self._width = max([abs(edge[1] - edge[0]) for edge in edges])
self._height = math.ceil((max(vertices) + 1) / self.get_width())
# Add vertices and edges
for vertex in vertices:
self.add_vertex(vertex)
for edge in edges:
self.add_edge(edge[0], edge[1], edge[2])
##########################################################################################
##########################################################################################