Source code for amaze.bin.main

#!/usr/bin/env python3
"""
Main executable for the AMaze library.

Provides and all-in-one entry point for simulation, evaluation and
 visualization
"""

import argparse
import logging
import pprint
from dataclasses import dataclass, field
from importlib import import_module
from pathlib import Path
from typing import Optional, Sequence

from ..misc.utils import qt_application, qt_offscreen
from ..simu.controllers.control import load, builtin_controllers, controller_factory
from ..simu.maze import Maze
from ..simu.robot import Robot
from ..simu.simulation import Simulation
from ..visu.viewer import MainWindow
from ..visu.widgets.maze import MazeWidget


[docs] @dataclass class Options: """ Namespace containing all options for the AMaze main executable """ maze: Optional[str] = None """ String description of the maze :see: :meth:`~amaze.simu.maze.Maze.to_string` :meth:`~amaze.simu.maze.Maze.from_string` """ robot: Optional[str] = None """ String description of the robot's attributes :see: :meth:`~amaze.simu.robot.Robot.BuildData.to_string` :meth:`~amaze.simu.robot.Robot.BuildData.from_string` """ controller: Optional[str] = None """ Path to a pre-trained controller or name of built-in. :see: :meth:`~amaze.simu.controllers.control.builtin_controllers` :note: if a pre-trained controller is provided, the extension should be .zip """ # ===================== is_robot: bool = False """ Activate "robot-mode" interface. No global view of the maze, only local perceptions and the instantaneous and cumulative rewards """ eval: Optional[Path] = None """ Path under which to store evaluation results """ eval_inputs: Optional[Path] = None """ Path under which to store input evaluation results """ autostart: bool = True """ Whether to directly start moving the agent around """ autoquit: bool = False """ Whether to close the viewer once the simulation is done """ restore_config: bool = True """ Whether to load configuration (window position, maze, robot, ...) from the persistent cache """ dt: Optional[float] = None """ Specifies the duration of a timestep when using the viewer""" # ===================== movie: Optional[Path] = None """ Where to store the movie of the agent's trajectory """ render: Optional[Path] = None """ Where to render the maze """ trajectory: Optional[Path] = None """ Where to render the agent's trajectory """ width: Optional[int] = None """ Width of images / videos """ cell_width: Optional[int] = None """ Width of a maze cell, to ensure readable graphics """ # ===================== dark: bool = False """ Whether to render a black on white maze (like the robots do) """ colorblind: bool = False """ Whether to use a colorblind-friendly palette """ # ===================== extensions: list[str] = field(default_factory=list) # ===================== verbose: int = 0 """ Verbosity level of the program """ # ===================== @staticmethod def populate(parser: argparse.ArgumentParser): group = parser.add_argument_group( "Maze", "Initial settings for maze generation" ) Maze.BuildData.populate_argparser(group) group = parser.add_argument_group( "Robot", "Initial settings for robot configuration" ) Robot.BuildData.populate_argparser(group) parser.add_argument( "--maze", dest="maze", help="Use provided string-format maze" ) parser.add_argument( "--robot", dest="robot", help="Use provided string-format robot details", ) parser.add_argument( "--auto-start", dest="autostart", action="store_false", help="Whether to autostart the evaluation", ) parser.add_argument( "--no-auto-start", dest="autostart", action="store_false", help="see autostart", ) parser.add_argument( "--auto-quit", dest="autoquit", action="store_true", help="Whether to quit after completing a maze", ) parser.add_argument( "--no-auto-quit", dest="autoquit", action="store_false", help="see autoquit", ) parser.add_argument( "--dt", dest="dt", type=float, help="Specifies the duration of one timestep when" " using the viewer", ) parser.add_argument( "--no-restore-config", dest="restore_config", action="store_false", help="Prevents configuration load from persistent" " cache", ) parser.add_argument( "--controller", dest="controller", type=str, help="Load robot/controller from file" "or force use of a builtin" "(keyboard or random)", ) parser.add_argument( "--robot-mode", dest="is_robot", action="store_true", help="See the maze as if you were a robot", ) parser.add_argument( "--evaluate", dest="eval", type=Path, help="Evaluate provided controller on provided" " maze and store results under the provided" " folder", ) parser.add_argument( "--evaluate-inputs", dest="eval_inputs", type=Path, help="Evaluate provided controller on all possible" " inputs according to the provided maze's" " signs and store the results under the" " provided folder", ) parser.add_argument( "--render", dest="render", type=Path, nargs="?", const="maze.png", help="Render maze to requested file", ) parser.add_argument( "--movie", dest="movie", type=Path, help="Render a video of the robot's strategy to requested file (gif)", ) parser.add_argument( "--trajectory", dest="trajectory", type=Path, nargs="?", const="trajectory.png", help="Plot trajectory of provided agent to provided path", ) parser.add_argument( "--width", type=int, default=None, help="Window or offscreen rendering target width", ) parser.add_argument( "--cell-width", type=int, help="Offscreen rendering target width of a single cell", ) parser.add_argument( "--dark", action="store_true", help="Dark background?" " (identical to agent's perceptions)", ) parser.add_argument( "--colorblind", action="store_true", help="Use colorblind-friendly colormaps", ) parser.add_argument( "--extension", action="append", dest="extensions", help="Request extension specific controllers to be made available", ) parser.add_argument( "-v", "--verbose", action="count", dest="verbose", help="Verbosity level of the program", )
def __make_simulation(args, trajectory=False): maze = __make_maze(args) robot = __make_robot(args) if args.cell_width is not None: args.width = args.cell_width * maze.width simulation = Simulation(maze, robot, save_trajectory=trajectory) controller = __make_controller(args, robot, simulation=simulation) return simulation, controller def __make_robot(args): if args.robot: robot = Robot.BuildData.from_string(args.robot).override_with( Robot.BuildData.from_argparse(args, set_defaults=False) ) else: robot = Robot.BuildData.from_argparse(args, set_defaults=True) return robot def __make_maze(args): if args.maze: maze_bd = Maze.BuildData.from_string( args.maze, Maze.BuildData.from_argparse(args, set_defaults=False) ) else: maze_bd = Maze.BuildData.from_argparse(args, set_defaults=True) return Maze.generate(maze_bd) def __make_controller( args, robot: Optional[Robot.BuildData] = None, simulation: Optional[Simulation] = None, ): if not args.controller: return None _robot = robot or Robot.BuildData.from_argparse(args, set_defaults=True) if args.controller in builtin_controllers(): controller = controller_factory( args.controller, dict(robot_data=_robot, simulation=simulation) ) else: controller = load(args.controller) if robot: robot.override_with(Robot.BuildData.from_controller(controller)) return controller
[docs] def main(sys_args: Optional[Sequence[str] | str] = None): """ Main function for the AMaze executable. Allows delegate call. """ if isinstance(sys_args, str): sys_args = sys_args.split() elif sys_args is not None: sys_args = [str(arg) for arg in sys_args] args = Options() parser = argparse.ArgumentParser(description="2D Maze environment") Options.populate(parser) parser.parse_args(args=sys_args, namespace=args) if args.verbose > 0: print("Executing with:", sys_args) if args.eval and not args.controller: raise ValueError("Cannot evaluate without a controller") if args.trajectory and not args.controller and not args.is_robot: raise ValueError("Cannot plot trajectory without a controller") if args.extensions: # pragma: no cover for m in args.extensions: import_module("amaze.extensions." + m) if not args.controller: args.autostart = False else: if args.controller.endswith(".zip"): p = Path(args.controller) if not p.exists(): raise ValueError(f"No controller archive found for '{p}'") args.controller = p if args.movie: args.autostart = True args.autoquit = True args.dt = 0 if args.eval: if args.render and len(args.render.parts) == 1: args.render = args.eval.joinpath(args.render) if args.trajectory and len(args.trajectory.parts) == 1: args.trajectory = args.eval.joinpath(args.trajectory) args.eval.mkdir(parents=True, exist_ok=True) if args.eval_inputs: args.eval_inputs.mkdir(parents=True, exist_ok=True) simulate = bool(args.eval or (args.trajectory and not args.is_robot)) window = not (args.render or args.eval_inputs or simulate) if not window: qt_offscreen() app = qt_application(start_offscreen=not window or args.movie) logging.basicConfig(level=logging.DEBUG) if args.eval_inputs: controller = __make_controller(args) if controller is None: raise ValueError("Cannot evaluate inputs without a controller") print(f"[kgd-debug|{__name__}]", "eval_inputs") res = Simulation.inputs_evaluation( args.eval_inputs, controller, __make_maze(args).signs ) pprint.pprint(res) if args.render: maze = __make_maze(args) if MazeWidget.static_render_to_file( maze, args.render, width=args.width, cell_width=args.cell_width, config=dict( robot=False, solution=True, dark=args.dark, colorblind=args.colorblind, ), ): print(f"Saved {maze.to_string()} to {args.render}") else: print(f"Failed to save {maze.to_string()} to {args.render}") if not window: simulation, controller = __make_simulation(args) # if args.render: # widget = MazeWidget.from_simulation( # simulation, # config=dict( # robot=False, # solution=True, # dark=args.dark, # colorblind=args.colorblind, # ), # ) # if widget.render_to_file(args.render, width=args.width): # print(f"Saved {simulation.maze.to_string()}" f" to {args.render}") if simulate: simulation.reset(save_trajectory=True) while not simulation.done(): simulation.step(controller(simulation.observations)) reward = simulation.robot.reward print( f"Cumulative reward: {reward} " f"{simulation.infos()['pretty_reward']}" ) if args.trajectory: MazeWidget.plot_trajectory( simulation=simulation, size=args.width or 256, path=args.trajectory, ) print( f"Plotted {args.controller}" f" in {simulation.maze.to_string()}" f" to {args.trajectory}" ) else: window = MainWindow(args) window.reset() window.show() if args.autostart: window.start() r = app.exec() return r return 0
if __name__ == "__main__": # pragma: no cover main()