diff --git a/.gitignore b/.gitignore index 23e18b78..5aab0a1b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,12 @@ +*.pkl +*.env +agents_buffer +syllabus/examples/experimental/round_robin/DR_SP_DR_FSP/*.json +syllabus/examples/experimental/round_robin/DR_SP_DR_PFSP/*.json +syllabus/examples/experimental/round_robin/DR_FSP_DR_PFSP/*.json +.neptune + +wandb # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..a6bd9482 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,46 @@ +FROM nvcr.io/nvidia/pytorch:23.06-py3 + +# Ensure no installs try to launch interactive screen +ARG DEBIAN_FRONTEND=noninteractive + +# Update packages and install dependencies +RUN apt-get update -y && \ + apt-get install -y software-properties-common git && \ + # CMake, Zlib development files, and build-essential + apt-get install -y cmake zlib1g-dev build-essential && \ + # adds the DeadSnakes PPA, which provides newer Python versions + add-apt-repository -y ppa:deadsnakes/ppa && \ + # python 3.10, development headers, pip, and python virtual environment package + apt-get install -y python3.10 python3.10-dev python3-pip python3.10-venv && \ + # use Python 3.10 as the default python command + update-alternatives --install /usr/bin/python python /usr/bin/python3.10 10 && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* + +# Setup virtual env and path +ENV VIRTUAL_ENV=/syllabus +ENV PATH=/syllabus/bin:$PATH + +# Set working directory +WORKDIR /home/app/syllabus + +# Copy requirements file first to leverage Docker cache +COPY requirements.txt . + +# Install requirements +RUN pip install --quiet --upgrade pip setuptools wheel && \ + pip install -r requirements.txt + +# Copy all code to the container +COPY . . + +RUN echo "Installing additional libraries ..." +RUN pip install multi-agent-ale-py griddly pettingzoo pufferlib \ + tensorboard pygame tqdm tyro colorama neptune python-dotenv && \ + pip install "autorom[accept-rom-license]" && \ + pip install jupyter ipykernel +RUN AutoROM --accept-license + +# expose the Jupyter notebook port +EXPOSE 8888 + diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..c9b5b37d --- /dev/null +++ b/Makefile @@ -0,0 +1,33 @@ +# Check if GPU is available +NVCC_RESULT := $(shell which nvcc 2> NULL) +NVCC_TEST := $(notdir $(NVCC_RESULT)) +ifeq ($(NVCC_TEST),nvcc) +GPUS=--gpus all +else +GPUS= +endif + +# For Windows use CURDIR +ifeq ($(PWD),) +PWD := $(CURDIR) +endif + +# Set flag for docker run command +BASE_FLAGS=-it --rm --shm-size=1g -v ${PWD}:/home/app/syllabus -w /home/app/syllabus +RUN_FLAGS=$(GPUS) $(BASE_FLAGS) + +DOCKER_RUN=docker run $(RUN_FLAGS) syllabus:latest +USE_CUDA = $(if $(GPUS),true,false) + +# make file commands +build: + DOCKER_BUILDKIT=1 docker build --build-arg USE_CUDA=$(USE_CUDA) --tag $(IMAGE) . + +run: + $(DOCKER_RUN) python $(example) + +bash: + $(DOCKER_RUN) bash + +jupyter: + $(DOCKER_RUN) jupyter notebook --ip=0.0.0.0 --port=8888 --no-browser --allow-root \ No newline at end of file diff --git a/lasertag/__init__.py b/lasertag/__init__.py new file mode 100644 index 00000000..e9d15082 --- /dev/null +++ b/lasertag/__init__.py @@ -0,0 +1,3 @@ +from .generator import * +from .custom_test import * +from .lasertag import * diff --git a/lasertag/custom_test.py b/lasertag/custom_test.py new file mode 100644 index 00000000..372c4e53 --- /dev/null +++ b/lasertag/custom_test.py @@ -0,0 +1,361 @@ +import gym.envs.registration as register +import numpy as np + +from .lasertag import Lasertag + + +class LasertagFixed(Lasertag): + """A short but non-optimal path is 80 moves.""" + + def __init__(self, *args, n_agents, level, max_steps=200, **kwargs): + self.level = level + self.max_steps = max_steps + super().__init__(*args, n_agents=n_agents, max_steps=max_steps, **kwargs) + + def reset(self): + return super().reset(level=self.level) + + +arena_1 = np.array( + [ + [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", "."], + [".", "w", ".", "w", ".", "w", ".", "w", ".", "w", ".", "w", "."], + [".", "w", "w", "w", "w", "w", "w", "w", "w", "w", "w", "w", "."], + [".", "w", "w", "w", "w", "w", "w", "w", "w", "w", "w", "w", "."], + ["a1", "w", "w", "w", "w", "w", "w", "w", "w", "w", "w", "w", "a2"], + [".", "w", "w", "w", "w", "w", "w", "w", "w", "w", "w", "w", "."], + [".", "w", "w", "w", "w", "w", "w", "w", "w", "w", "w", "w", "."], + [".", "w", ".", "w", ".", "w", ".", "w", ".", "w", ".", "w", "."], + [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", "."], + ] +) + +arena_2 = np.array( + [ + [".", "w", "w", "w", "w", "w", "w", "w", "w", "w", "w", "w", "."], + ["a1[D]", "w", "w", "w", "w", "w", "w", "w", "w", "w", "w", "w", "."], + [".", "w", "w", "w", "w", "w", "w", "w", "w", "w", "w", "w", "."], + [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", "."], + [".", "w", ".", "w", ".", "w", ".", "w", ".", "w", ".", "w", "."], + [".", "w", "w", "w", "w", "w", "w", "w", "w", "w", "w", "w", "a2[U]"], + [".", "w", "w", "w", "w", "w", "w", "w", "w", "w", "w", "w", "."], + ] +) + +corridor_1 = np.array( + [ + ["a1[D]", "w", "w", "w", "."], + [".", "w", "w", "w", "."], + [".", "w", "w", "w", "."], + [".", ".", ".", ".", "."], + [".", "w", "w", "w", "."], + [".", "w", "w", "w", "."], + [".", "w", "w", "w", "a2[U]"], + ] +) + +corridor_2 = np.array( + [ + [".", ".", ".", ".", "."], + [".", "w", "w", "w", "."], + [".", "w", "w", "w", "."], + ["a1", "w", "w", "w", "a2"], + [".", "w", "w", "w", "."], + [".", "w", "w", "w", "."], + [".", ".", ".", ".", "."], + ] +) + +maze1 = np.array( + [ + [".", ".", ".", ".", ".", "w", "a2", ".", ".", ".", "w", ".", "."], + [".", "w", "w", "w", ".", "w", "w", "w", "w", ".", "w", "w", "."], + [".", "w", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", "."], + [".", "w", "w", "w", "w", "w", "w", "w", "w", ".", "w", "w", "w"], + [".", ".", ".", ".", ".", ".", ".", ".", "w", ".", ".", ".", "."], + ["w", "w", "w", "w", "w", "w", ".", "w", "w", "w", "w", "w", "."], + [".", ".", ".", ".", "w", ".", ".", "w", ".", ".", ".", ".", "."], + [".", "w", "w", ".", ".", ".", "w", "w", ".", "w", "w", "w", "w"], + [".", ".", "w", ".", "w", ".", ".", "w", ".", ".", ".", "w", "."], + ["w", ".", "w", ".", "w", "w", ".", "w", "w", "w", ".", "w", "."], + ["w", ".", "w", ".", ".", "w", ".", ".", ".", "w", ".", ".", "."], + ["w", ".", "w", "w", ".", "w", "w", "w", ".", "w", "w", "w", "."], + [".", ".", ".", "w", ".", ".", "a1", "w", ".", "w", ".", ".", "."], + ] +) + +maze2 = np.array( + [ + [".", ".", ".", "w", ".", "w", ".", ".", ".", ".", "w", ".", "."], + [".", "w", ".", "w", ".", "w", "w", "w", "w", ".", ".", ".", "w"], + [".", "w", ".", ".", ".", ".", ".", ".", ".", ".", "w", ".", "."], + [".", "w", "w", "w", "w", "w", "w", "w", "w", ".", "w", "w", "w"], + [".", ".", ".", "w", ".", ".", "w", ".", "w", ".", "w", ".", "a2"], + ["w", "w", ".", "w", ".", "w", "w", ".", "w", ".", "w", ".", "."], + ["a1", "w", ".", "w", ".", ".", ".", ".", "w", ".", "w", "w", "."], + [".", "w", ".", "w", "w", ".", "w", "w", "w", ".", ".", "w", "."], + [".", "w", ".", ".", "w", ".", ".", "w", "w", "w", ".", "w", "."], + [".", "w", "w", ".", "w", "w", ".", "w", ".", "w", ".", "w", "."], + [".", "w", ".", ".", ".", "w", ".", "w", ".", "w", ".", "w", "."], + [".", "w", ".", "w", ".", "w", ".", "w", ".", "w", ".", "w", "."], + [".", ".", ".", "w", ".", ".", ".", "w", ".", ".", ".", ".", "."], + ] +) + +ruins = np.array( + [ + [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", "."], + [".", "w", ".", "w", ".", "w", ".", "w", ".", "w", ".", "w", "."], + ["a1[R]", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", "."], + [".", "w", ".", "w", ".", "w", ".", "w", ".", "w", ".", "w", "."], + [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", "."], + [".", "w", ".", "w", ".", "w", ".", "w", ".", "w", ".", "w", "."], + [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", "."], + [".", "w", ".", "w", ".", "w", ".", "w", ".", "w", ".", "w", "."], + [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", "."], + [".", "w", ".", "w", ".", "w", ".", "w", ".", "w", ".", "w", "."], + [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", "a2[L]"], + [".", "w", ".", "w", ".", "w", ".", "w", ".", "w", ".", "w", "."], + [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", "."], + ] +) + +ruins_2 = np.array( + [ + ["a1[D]", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", "."], + [".", "w", "w", ".", ".", "w", "w", ".", ".", "w", "w", "."], + [".", "w", "w", ".", ".", "w", "w", ".", ".", "w", "w", "."], + [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", "."], + [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", "."], + [".", "w", "w", ".", ".", "w", "w", ".", ".", "w", "w", "."], + [".", "w", "w", ".", ".", "w", "w", ".", ".", "w", "w", "."], + [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", "."], + [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", "."], + [".", "w", "w", ".", ".", "w", "w", ".", ".", "w", "w", "."], + [".", "w", "w", ".", ".", "w", "w", ".", ".", "w", "w", "."], + [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", "a2[U]"], + ] +) + +star = np.array( + [ + ["w", "w", ".", "w", "w", "w", ".", "w", "w", "w", ".", "w", "w"], + ["w", ".", ".", ".", "w", ".", ".", ".", "w", ".", ".", ".", "w"], + [".", ".", "w", ".", ".", ".", "w", ".", ".", ".", "w", ".", "a2[D]"], + [".", "w", "w", "w", ".", "w", "w", "w", ".", "w", "w", "w", "."], + [".", ".", "w", ".", ".", ".", "w", ".", ".", ".", "w", ".", "."], + ["w", ".", ".", ".", "w", ".", ".", ".", "w", ".", ".", ".", "w"], + ["w", "w", ".", "w", "w", "w", ".", "w", "w", "w", ".", "w", "w"], + ["w", ".", ".", ".", "w", ".", ".", ".", "w", ".", ".", ".", "w"], + [".", ".", "w", ".", ".", ".", "w", ".", ".", ".", "w", ".", "."], + [".", "w", "w", "w", ".", "w", "w", "w", ".", "w", "w", "w", "."], + ["a1[U]", ".", "w", ".", ".", ".", "w", ".", ".", ".", "w", ".", "."], + ["w", ".", ".", ".", "w", ".", ".", ".", "w", ".", ".", ".", "w"], + ["w", "w", ".", "w", "w", "w", ".", "w", "w", "w", ".", "w", "w"], + ] +) + +corridor_large = np.array( + [ + [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", "."], + [".", "w", ".", "w", ".", "w", ".", "w", ".", "w", "."], + [".", "w", ".", "w", ".", "w", ".", "w", ".", "w", "."], + [".", "w", ".", "w", ".", "w", ".", "w", ".", "w", "."], + [".", "w", "a1[U]", "w", ".", "w", ".", "w", ".", "w", "."], + [".", "w", "w", "w", "w", "w", "w", "w", "w", "w", "."], + [".", "w", ".", "w", ".", "w", ".", "w", "a2[D]", "w", "."], + [".", "w", ".", "w", ".", "w", ".", "w", ".", "w", "."], + [".", "w", ".", "w", ".", "w", ".", "w", ".", "w", "."], + [".", "w", ".", "w", ".", "w", ".", "w", ".", "w", "."], + [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", "."], + ] +) + +cross = np.array( + [ + ["a1[R]", ".", ".", ".", ".", ".", ".", ".", ".", ".", "."], + [".", ".", ".", ".", "w", ".", "w", ".", ".", ".", "."], + [".", ".", ".", ".", "w", ".", "w", ".", ".", ".", "."], + [".", ".", ".", ".", "w", ".", "w", ".", ".", ".", "."], + [".", "w", "w", "w", "w", ".", "w", "w", "w", "w", "."], + [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", "."], + [".", "w", "w", "w", "w", ".", "w", "w", "w", "w", "."], + [".", ".", ".", ".", "w", ".", "w", ".", ".", ".", "."], + [".", ".", ".", ".", "w", ".", "w", ".", ".", ".", "."], + [".", ".", ".", ".", "w", ".", "w", ".", ".", ".", "."], + [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", "a2[L]"], + ] +) + + +fourrooms_2 = np.array( + [ + [".", ".", ".", ".", ".", ".", "w", ".", ".", ".", ".", ".", "."], + [".", ".", ".", ".", ".", ".", "w", ".", ".", ".", ".", ".", "."], + [".", ".", "a1[D]", ".", ".", ".", "w", ".", ".", ".", ".", ".", "."], + [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", "."], + [".", ".", ".", ".", ".", ".", "w", ".", ".", ".", ".", ".", "."], + [".", ".", ".", ".", ".", ".", "w", ".", ".", ".", ".", ".", "."], + ["w", "w", "w", ".", "w", "w", "w", "w", "w", ".", "w", "w", "w"], + [".", ".", ".", ".", ".", ".", "w", ".", ".", ".", ".", ".", "."], + [".", ".", ".", ".", ".", ".", "w", ".", ".", ".", ".", ".", "."], + [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", "."], + [".", ".", ".", ".", ".", ".", "w", ".", ".", ".", "a2[U]", ".", "."], + [".", ".", ".", ".", ".", ".", "w", ".", ".", ".", ".", ".", "."], + [".", ".", ".", ".", ".", ".", "w", ".", ".", ".", ".", ".", "."], + ] +) + +sixteenrooms_2 = np.array( + [ + [".", ".", ".", "w", ".", ".", "w", ".", ".", "w", ".", ".", "."], + [".", "a1[D]", ".", ".", ".", ".", ".", ".", ".", "w", ".", ".", "."], + [".", ".", ".", "w", ".", ".", "w", ".", ".", ".", ".", ".", "."], + ["w", ".", "w", "w", "w", ".", "w", "w", ".", "w", "w", "w", "."], + [".", ".", ".", "w", ".", ".", ".", ".", ".", ".", ".", ".", "."], + [".", ".", ".", ".", ".", ".", "w", ".", ".", "w", ".", ".", "."], + ["w", "w", ".", "w", ".", "w", "w", ".", "w", "w", "w", ".", "w"], + [".", ".", ".", "w", ".", ".", ".", ".", ".", "w", ".", ".", "."], + [".", ".", ".", "w", ".", ".", "w", ".", ".", ".", ".", ".", "."], + [".", "w", "w", "w", "w", ".", "w", "w", ".", "w", ".", "w", "w"], + [".", ".", ".", "w", ".", ".", "w", ".", ".", "w", ".", ".", "."], + [".", ".", ".", ".", ".", ".", "w", ".", ".", ".", ".", "a2[L]", "."], + [".", ".", ".", "w", ".", ".", ".", ".", ".", "w", ".", ".", "."], + ] +) + + +class LasertagArena1(LasertagFixed): + def __init__(self, *args, **kwargs): + super().__init__(*args, n_agents=2, level=arena_1, **kwargs) + + +class LasertagArena2(LasertagFixed): + def __init__(self, *args, **kwargs): + super().__init__(*args, n_agents=2, level=arena_2, **kwargs) + + +class LasertagCorridor1(LasertagFixed): + def __init__(self, *args, **kwargs): + super().__init__(*args, n_agents=2, level=corridor_1, **kwargs) + + +class LasertagCorridor2(LasertagFixed): + def __init__(self, *args, **kwargs): + super().__init__(*args, n_agents=2, level=corridor_2, **kwargs) + + +class LasertagMaze1(LasertagFixed): + def __init__(self, *args, **kwargs): + super().__init__(*args, n_agents=2, level=maze1, max_steps=350, **kwargs) + + +class LasertagMaze2(LasertagFixed): + def __init__(self, *args, **kwargs): + super().__init__(*args, n_agents=2, level=maze2, max_steps=350, **kwargs) + + +class LasertagRuins(LasertagFixed): + def __init__(self, *args, **kwargs): + super().__init__(*args, n_agents=2, level=ruins, max_steps=250, **kwargs) + + +class LasertagRuins2(LasertagFixed): + def __init__(self, *args, **kwargs): + super().__init__(*args, n_agents=2, level=ruins_2, max_steps=250, **kwargs) + + +class LasertagStar(LasertagFixed): + def __init__(self, *args, **kwargs): + super().__init__(*args, n_agents=2, level=star, max_steps=350, **kwargs) + + +class LasertagCross(LasertagFixed): + def __init__(self, *args, **kwargs): + super().__init__(*args, n_agents=2, level=cross, max_steps=350, **kwargs) + + +class LasertagLargeCorridor(LasertagFixed): + def __init__(self, *args, **kwargs): + super().__init__( + *args, n_agents=2, level=corridor_large, max_steps=350, **kwargs + ) + + +class LasertagFourRoomsN2(LasertagFixed): + def __init__(self, *args, **kwargs): + super().__init__(*args, n_agents=2, level=fourrooms_2, max_steps=250, **kwargs) + + +class LasertagSixteenRoomsN2(LasertagFixed): + def __init__(self, *args, **kwargs): + super().__init__( + *args, n_agents=2, level=sixteenrooms_2, max_steps=250, **kwargs + ) + + +def set_global(name, value): + globals()[name] = value + + +if hasattr(__loader__, "name"): + module_path = __loader__.name +elif hasattr(__loader__, "fullname"): + module_path = __loader__.fullname + + +register.register( + id="Lasertag-Arena1-N2-v0", + entry_point=module_path + ":LasertagArena1", +) + + +register.register( + id="Lasertag-Arena2-N2-v0", + entry_point=module_path + ":LasertagArena2", +) + +register.register( + id="Lasertag-Corridor1-N2-v0", + entry_point=module_path + ":LasertagCorridor1", +) +register.register( + id="Lasertag-Corridor2-N2-v0", + entry_point=module_path + ":LasertagCorridor2", +) +register.register( + id="Lasertag-Maze1-N2-v0", + entry_point=module_path + ":LasertagMaze1", +) +register.register( + id="Lasertag-Maze2-N2-v0", + entry_point=module_path + ":LasertagMaze2", +) +register.register( + id="Lasertag-Ruins-N2-v0", + entry_point=module_path + ":LasertagRuins", +) +register.register( + id="Lasertag-Ruins2-N2-v0", + entry_point=module_path + ":LasertagRuins2", +) +register.register( + id="Lasertag-Star-N2-v0", + entry_point=module_path + ":LasertagStar", +) +register.register( + id="Lasertag-Cross-N2-v0", + entry_point=module_path + ":LasertagCross", +) +register.register( + id="Lasertag-FourRooms-N2-v0", + entry_point=module_path + ":LasertagFourRoomsN2", +) +register.register( + id="Lasertag-SixteenRooms-N2-v0", + entry_point=module_path + ":LasertagSixteenRoomsN2", +) +register.register( + id="Lasertag-LargeCorridor-N2-v0", + entry_point=module_path + ":LasertagLargeCorridor", +) diff --git a/lasertag/gdy/lasertag_wall_general_zs.yaml b/lasertag/gdy/lasertag_wall_general_zs.yaml new file mode 100644 index 00000000..8983587d --- /dev/null +++ b/lasertag/gdy/lasertag_wall_general_zs.yaml @@ -0,0 +1,194 @@ +Version: "0.1" +Environment: + Name: Laser Tag + Observers: + Sprite2D: + TileSize: 24 + BackgroundTile: Passage.png + Block2D: + TileSize: 32 + BackgroundColor: [0.0,0.0,0.0] + Shader: + ObserverAvatarMode: HIGHLIGHT + # ObserverAvatarHighlightColor: [0.1, 0.2, 0.35] + ObserverAvatarHighlightColor: [0.3, 0.3, 0.3] + Vector: + IncludePlayerID: false + IncludeRotation: false + IncludeVariables: false + Player: + Colors: + - [1.0, 0.2, 0.4] + - [0.27, 0.69, 0.99] + - [0.0, 1.0, 0.0] + Count: $N_AGENT + AvatarObject: agent # TODO set this programmatically + Observer: + HighlightPlayers: false # Add highlights to the players in visual observers. + RotateWithAvatar: true # rotate to follow the orientation of the avatar. + TrackAvatar: true # The observer view will track the position of the avatar + Height: 5 + Width: 5 + OffsetX: 0 + OffsetY: 2 + Variables: + - Name: dummy + InitialValue: 1 + Termination: + End: + - eq: [ dummy, 0 ] # make sure episode doesn't terminate + Levels: + - | + . . . . . . . . a2 + . . . . . . . . . + . . . w w w w . . + . . . . . w w . . + a1 . . w . . w . . + . . . w w . . . . + . . . w w w w . w + . . . . . . . . . + . . . . w . . . . + +Actions: + + - Name: flame_projectile_movement + InputMapping: + Internal: true + Behaviours: + - Src: + Object: flame + Commands: + - mov: _dest + - eq: + Arguments: [ range, 0 ] + Commands: + - remove: true + - gt: + Arguments: [ range, 0 ] + Commands: + - decr: range + - exec: + Action: flame_projectile_movement + Delay: 1 + Dst: + Object: _empty + - Src: # Flame -> wall + Object: flame + Commands: + - remove: true + Dst: + Object: wall + # Commands: + # - remove: true + - Src: # Flame -> Agent + Object: flame + Commands: + - remove: true + - reward: 1 + Dst: + Object: agent + Commands: + - remove: true + - reward: -1 + - Src: # Flame -> [Flame, Border] + Object: flame + Commands: + - remove: true + Dst: + Object: [flame, _boundary] + + - Name: stumble + InputMapping: + Inputs: + 1: + Description: Rotate left + OrientationVector: [-1, 0] + 2: + Description: Move forwards + OrientationVector: [0, -1] + VectorToDest: [0, -1] + 3: + Description: Rotate right + OrientationVector: [1, 0] + Relative: true + Behaviours: + # Tell the agent to rotate if the agent performs an action on itself + - Src: + Object: agent + Commands: + - rot: _dir + Dst: + Object: agent + + # The agent can move around freely in empty and always rotates the direction it is travelling + - Src: + Object: agent + Commands: + - mov: _dest + Dst: + Object: _empty + + - Name: flame_shoot + InputMapping: + Inputs: + 1: # Only shoot the direction the agent is facing + OrientationVector: [ 0, -1 ] + VectorToDest: [ 0, -1 ] + Relative: true + Behaviours: + - Src: + Object: agent + Dst: + Object: _empty + Commands: + - spawn: flame + + +Objects: + - Name: wall + Z: 2 + MapCharacter: w + Observers: + Isometric: + - Image: oryx/oryx_iso_dungeon/crate-1.png + Sprite2D: + - Image: Rigid.png + Block2D: + - Shape: square + Color: [0.7, 0.7, 0.7] + Scale: 1.0 + + - Name: agent + Z: 2 + MapCharacter: a + Observers: + Isometric: + - Image: oryx/oryx_iso_dungeon/jelly-1.png + Sprite2D: + - Image: Agent0.png + Block2D: + - Shape: triangle + Color: PLAYER + Scale: 1.0 + + - Name: flame + Z: 2 + InitialActions: + - Action: flame_projectile_movement + Delay: 2 + Variables: + - Name: range + InitialValue: 60 + MapCharacter: f + Observers: + Isometric: + - Image: oryx/oryx_iso_dungeon/fire-pink-up.png + - Image: oryx/oryx_iso_dungeon/fire-pink-right.png + - Image: oryx/oryx_iso_dungeon/fire-pink-down.png + - Image: oryx/oryx_iso_dungeon/fire-pink-left.png + Sprite2D: + - Image: Flames.png + Block2D: + - Shape: circle + Color: [1.0, 0.0, 0.0] + Scale: 0.4 \ No newline at end of file diff --git a/lasertag/generator.py b/lasertag/generator.py new file mode 100644 index 00000000..7e8d05b9 --- /dev/null +++ b/lasertag/generator.py @@ -0,0 +1,377 @@ +import networkx as nx +import numpy as np +from networkx import grid_graph + +from . import custom_test, grid, lasertag, register +class hashabledict(dict): + def __hash__(self): + return hash(tuple(sorted(self.items()))) + + +class LasertagAdversarial(lasertag.Lasertag): + """Adversarial environment for multi-agent laser tag.""" + + def __init__( + self, + *args, + n_agents=2, # (maximum) number of agents + min_agents=2, # minimum number of agents + min_size=4, # minimum size of the square grid (excluding outer walls) + max_size=10, # maximum size of the square grid (excludign outer walls) + max_clutter_rate=0.5, # How much of the area can be clutter (e.g. walls) + min_clutter_rate=0, # How much of the area can be clutter (e.g. walls) + agent_view_size=5, # agent view size + max_steps=200, + seed=0, + fixed_environment=None, + **kwargs, + ): + """Initializes environment in which adversary places goal, agent, obstacles. + + Args: + n_clutter: The maximum number of obstacles the adversary can place. + size: The number of tiles across one side of the grid; i.e. make a + size x size grid. + agent_view_size: The number of tiles in one side of the agent's partially + observed view of the grid. + max_steps: The maximum number of steps that can be taken before the + episode terminates. + """ + self.n_agents = n_agents + self.min_agents = min_agents + + self.max_size = max_size + self.min_size = min_size + + self.max_clutter_rate = max_clutter_rate + self.min_clutter_rate = min_clutter_rate + + self.fixed_environment = fixed_environment + + # Seed + self.seed(seed) + + super().__init__( + *args, + n_agents=n_agents, + agent_view_size=agent_view_size, + max_steps=max_steps, + **kwargs, + ) + + # Metrics + self.reset_metrics() + + # Generate the grid. + self.grid = grid.Grid( + n_agents=self.n_agents, width=self.max_size, height=self.max_size + ) + + def reset(self): + self.step_count = 0 + + # Extra metrics + self.reset_metrics() + + # Generate the grid. + self.grid = grid.Grid( + n_agents=self.n_agents, width=self.max_size, height=self.max_size + ) + + return super().reset(level=self.grid.level) + + def seed(self, seed=None): + np.random.seed(seed) + return seed + + @property + def processed_action_dim(self): + return 1 + + def reset_metrics(self): + self.n_clutter_placed = 0 + self.clutter_rate_selected = 0 + self.n_agents_selected = 0 + self.grid_size_selected = 0 + self.solvable = -1 + self.shortest_path_length = 0 + + def get_metrics(self): + metrics = hashabledict() + metrics["n_clutter_placed"] = self.n_clutter_placed + metrics["clutter_rate_selected"] = self.clutter_rate_selected + metrics["n_agents_selected"] = self.n_agents_selected + metrics["grid_size_selected"] = self.grid_size_selected + metrics["solvable"] = self.solvable + metrics["shortest_path_length"] = self.shortest_path_length + return metrics + + def set_metrics(self, **kwargs): + self.n_clutter_placed = kwargs["n_clutter_placed"] + self.clutter_rate_selected = kwargs["clutter_rate_selected"] + self.n_agents_selected = kwargs["n_agents_selected"] + self.grid_size_selected = kwargs["grid_size_selected"] + self.solvable = kwargs["solvable"] + self.shortest_path_length = kwargs["shortest_path_length"] + + def reset_agent(self, *args, **kwargs): + # Step count since episode start + self.step_count = 0 + + # Return first observation + return super().reset(*args, level=self.grid.level, **kwargs) + + @property + def level(self): + return self.grid.encode(self.get_metrics()) + + def reset_to_level(self, level): + metrics = self.grid.decode(level) + self.set_metrics(**metrics) + + return self.reset_agent() + + def reset_random(self): + """Use domain randomization to create the environment.""" + if self.fixed_environment is not None: + if self.fixed_environment == "easy": + self.grid.level = custom_test.corridor_easy + elif self.fixed_environment == "four_rooms": + self.grid.level = custom_test.fourrooms_2 + elif self.fixed_environment == "empty": + self.grid.level = custom_test.empty_2 + elif self.fixed_environment == "ruins": + self.grid.level = custom_test.ruins + else: + raise AttributeError("Wrong fixed env name") + + return self.reset_agent() + + self.step_count = 0 + self.adversary_step_count = 0 + + # Choose grid size and create an empty grid + size = np.random.randint(self.min_size, self.max_size + 1) + self.grid = grid.Grid(n_agents=self.n_agents, width=size, height=size) + + # Choose number of agents + n_agents = np.random.randint(self.min_agents, self.n_agents + 1) + # Randomly place agents + agent_ids = np.random.choice( + range(self.n_agents), n_agents, replace=False + ) + agent_dirs = np.random.choice(["U", "D", "L", "R"], n_agents) + agent_strs = [ + f"a{id+1}[{dir}]" for id, dir in zip(agent_ids, agent_dirs) + ] + agent_locs = np.random.choice( + range(size * size), n_agents, replace=False + ) + self.grid.put(agent_locs, agent_strs) + + # Randomly place walls + space = size * size - n_agents + clutter_rate = np.random.uniform( + low=self.min_clutter_rate, high=self.max_clutter_rate + ) + n_clutter = int(space * clutter_rate) + possible_locs = [x for x in range(size * size) if x not in agent_locs] + walls = np.random.choice(possible_locs, n_clutter, replace=False) + self.grid.put(walls, ["w"]) + + # Compute metrics + self.reset_metrics() + self.n_clutter_placed = n_clutter + self.clutter_rate_selected = clutter_rate + self.n_agents_selected = n_agents + self.grid_size_selected = size + + # Construct a graph of the grid + self.construct_graph(size=size, walls=walls, agent_locs=agent_locs) + self.solvable = self.is_solvable(agent_locs=agent_locs) + if self.solvable: + self.shortest_path_length = self.get_shortest_path( + agent_locs, n_agents + ) + else: + self.shortest_path_length = 0 + + return self.reset_agent() + + def construct_graph(self, size, walls, agent_locs): + """Construct the graph of the generated grid.""" + self.graph = grid_graph(dim=[size, size]) + + for w in walls: + if w not in agent_locs: + self.graph.remove_node(self.get_coord(w)) + + def get_coord(self, loc): + """Get coordinate from int location.""" + x = int(loc % (self.grid_size_selected)) + y = int(loc // (self.grid_size_selected)) + return x, y + + def is_solvable(self, agent_locs): + # Check if there is a path between agent 1 and other agents. + solvable = all( + nx.has_path( + self.graph, + source=self.get_coord(agent_locs[0]), + target=self.get_coord(next_agent), + ) + for next_agent in agent_locs[1:] + ) + return int(solvable) + + def get_shortest_path(self, agent_locs, n): + """Get the shortest path between all agents. Assumes a solvable grid""" + assert self.solvable + shortest_path = 0 + for a, b in zip(agent_locs, agent_locs[1:]): + shortest_path += nx.shortest_path_length( + self.graph, source=self.get_coord(a), target=self.get_coord(b) + ) + shortest_path = shortest_path / (n * (n - 1) / 2) + + return shortest_path + + +# 2 Agent - 5x5-15x15 - Walls [0, 50%] +class LasertagAdversarialN2SizeWalls(LasertagAdversarial): + def __init__( + self, + seed=0, + record_video=False, + video_filename="", + **kwargs, + ): + super().__init__( + seed=seed, + n_agents=2, + min_agents=2, + max_size=15, + min_size=5, + min_clutter_rate=0, + max_clutter_rate=0.5, + max_steps=300, + record_video=record_video, + video_filename=video_filename, + **kwargs, + ) + + +if hasattr(__loader__, "name"): + module_path = __loader__.name +elif hasattr(__loader__, "fullname"): + module_path = __loader__.fullname + + +register.register( + env_id="Lasertag-Adversarial-N2-Size-Walls-v0", + entry_point=module_path + ":LasertagAdversarialN2SizeWalls", + max_episode_steps=300, +) + + +# Fixed environment (four rooms) +class LasertagFixedFourRooms(LasertagAdversarial): + def __init__( + self, + seed=0, + record_video=False, + video_filename="", + **kwargs, + ): + super().__init__( + seed=seed, + n_agents=2, + min_agents=2, + max_steps=200, + fixed_environment="four_rooms", + record_video=record_video, + video_filename=video_filename, + **kwargs, + ) + + +class LasertagFixedEasy(LasertagAdversarial): + def __init__( + self, + seed=0, + record_video=False, + video_filename="", + **kwargs, + ): + super().__init__( + seed=seed, + n_agents=2, + min_agents=2, + max_steps=200, + fixed_environment="easy", + record_video=record_video, + video_filename=video_filename, + **kwargs, + ) + + +class LasertagFixedRuins(LasertagAdversarial): + def __init__( + self, + seed=0, + record_video=False, + video_filename="", + **kwargs, + ): + super().__init__( + seed=seed, + n_agents=2, + min_agents=2, + max_steps=200, + fixed_environment="ruins", + record_video=record_video, + video_filename=video_filename, + **kwargs, + ) + + +class LasertagFixedEmpty(LasertagAdversarial): + def __init__( + self, + seed=0, + record_video=False, + video_filename="", + **kwargs, + ): + super().__init__( + seed=seed, + n_agents=2, + min_agents=2, + max_steps=200, + fixed_environment="empty", + record_video=record_video, + video_filename=video_filename, + **kwargs, + ) + + +register.register( + env_id="Lasertag-Adversarial-Fixed-Easy-v0", + entry_point=module_path + ":LasertagFixedEasy", + max_episode_steps=200, +) +register.register( + env_id="Lasertag-Adversarial-Fixed-Empty-v0", + entry_point=module_path + ":LasertagFixedEmpty", + max_episode_steps=200, +) +register.register( + env_id="Lasertag-Adversarial-Fixed-Ruins-v0", + entry_point=module_path + ":LasertagFixedRuins", + max_episode_steps=200, +) +register.register( + env_id="Lasertag-Adversarial-Fixed-FourRooms-v0", + entry_point=module_path + ":LasertagFixedFourRooms", + max_episode_steps=200, +) diff --git a/lasertag/grid.py b/lasertag/grid.py new file mode 100644 index 00000000..1e5cf2ed --- /dev/null +++ b/lasertag/grid.py @@ -0,0 +1,54 @@ +import numpy as np + + +class Grid(object): + def __init__(self, n_agents, width, height, dtype="U5"): + self.n_agents = n_agents + self.width = width + self.height = height + self.dtype = dtype + self.level = np.full((width, height), ".", dtype=dtype) + + def put(self, indicies, values): + np.put(self.level, indicies, values) + + def set_level(self, level): + self.level = level + self.height = level.shape[0] + self.width = level.shape[1] + + def get_n_clutter(self): + return (self.level == "w").sum() + + def get_first_symbol(self, symbol): + try: + return ( + np.where(self.level == symbol)[1][0], + np.where(self.level == symbol)[0][0], + ) + except IndexError: + return None + + def get_agent_locs(self): + locs = [] + for a in range(self.n_agents): + symbol = "a" + str(a + 1) + loc = self.get_first_symbol(symbol) + locs.append(loc) + + return locs + + # for returning a level represntation + def encode(self, metrics): + return (self.level.tobytes("C"), self.width, self.height, metrics) + + def decode(self, level_encoding): + level_bytes = level_encoding[0] + self.width = level_encoding[1] + self.height = level_encoding[2] + + self.level = np.frombuffer(level_bytes, dtype=self.dtype).reshape( + (self.width, self.height) + ) + + return level_encoding[3] # metrics diff --git a/lasertag/lasertag.py b/lasertag/lasertag.py new file mode 100644 index 00000000..5ea6f29e --- /dev/null +++ b/lasertag/lasertag.py @@ -0,0 +1,263 @@ +import glob +import os +import shutil +import tempfile + +import gym +import numpy as np +import torch +import yaml + +# from gym.utils.play import play +from griddly import GymWrapper as GriddlyGymWrapper +from griddly import gd +from griddly.util.action_space import MultiAgentActionSpace + +try: + import PIL.Image +except ModuleNotFoundError: + raise ModuleNotFoundError( + "To safe GIF files of trajectories, please install Pillow:" + " pip install Pillow" + ) + + +class Lasertag(GriddlyGymWrapper): + def __init__( + self, + *args, + n_agents=5, # max number of agents, can be less than this + agent_view_size=5, # agent view size + record_video=False, # recording video during evaluation + video_filename="", # where to record the video + zero_sum=True, # Dead agents receive a reward of -1 + mask_actions=False, + survival_mode=False, # Only reward the agent for not dying + **kwargs, + ): + self.step_count = 0 + self.n_agents = n_agents + self.agent_view_size = agent_view_size + self.zero_sum = zero_sum + self.mask_actions = mask_actions + self.survival_mode = survival_mode + + self.record_video = record_video + self.recording_started = False + self.video_filename = video_filename + + yaml_filename = "gdy/lasertag_wall_general_zs.yaml" + + yaml_path = os.path.join( + os.path.dirname(os.path.realpath(__file__)), yaml_filename + ) + with open(yaml_path, "r") as stream: + yaml_dict = yaml.safe_load(stream) + # Fixing the number of agents + yaml_dict["Environment"]["Player"]["Count"] = self.n_agents + # Fixing the agent view size + yaml_dict["Environment"]["Player"]["Observer"]["Height"] = agent_view_size + yaml_dict["Environment"]["Player"]["Observer"]["Width"] = agent_view_size + yaml_dict["Environment"]["Player"]["Observer"]["OffsetY"] = 0 + yaml_dict["Environment"]["Player"]["Observer"]["OffsetY"] = ( + agent_view_size // 2 + ) + self.yaml_string = yaml.dump( + yaml_dict, default_flow_style=False, sort_keys=False + ) + + kwargs["yaml_string"] = self.yaml_string + kwargs["player_observer_type"] = kwargs.pop( + "player_observer_type", gd.ObserverType.VECTOR + ) + kwargs["global_observer_type"] = kwargs.pop( + "global_observer_type", gd.ObserverType.BLOCK_2D + ) + kwargs["max_steps"] = kwargs.pop("max_steps", 200) + + super().__init__(*args, **kwargs) + + self.action_map = { + 0: [0, 0], # no-op + 1: [0, 1], # left + 2: [0, 2], # move + 3: [0, 3], # right + 4: [1, 1], # shoot + } + self.n_actions = len(self.action_map) + + @property + def observation_space(self): + obs_dict = {} + self.image_obs_space = gym.spaces.Box( + low=0, + high=255, + shape=( + self.n_agents, + 3, + self.agent_view_size, + self.agent_view_size, + ), + dtype="uint8", + ) + obs_dict["image"] = self.image_obs_space + + if self.mask_actions: + self.avail_act_obs_space = gym.spaces.Box( + low=0, + high=1, + shape=(self.n_agents, self.n_actions), + dtype="bool", + ) + obs_dict["avail_actions"] = self.avail_act_obs_space + + return gym.spaces.Dict(obs_dict) + + @property + def action_space(self): + return MultiAgentActionSpace( + [gym.spaces.Discrete(self.n_actions) for _ in range(self.n_agents)] + ) + + def get_active_agents(self): + state = self.get_state() + active_agents = set() + + for object in state["Objects"]: + if object["Name"] == "agent": + active_agents.add(object["PlayerId"] - 1) + + return active_agents + + def step(self, actions): + if torch.is_tensor(actions): + actions = actions.cpu().numpy() + assert len(actions) == self.n_agents + + if self.mask_actions: + for a in range(self.n_agents): + # Make sure only available actions are chosen + avail_actions = self.get_avail_agent_actions(a) + assert avail_actions[actions[a]] == 1 + + actions = [self.action_map[a] for a in actions] + obs, reward, done, info = super().step(actions) + self.step_count += 1 + + if self.record_video: + frame = self.render(mode="rgb_array") + image = PIL.Image.fromarray(frame) + image.save(os.path.join(self.tmpdir, f"e_s_{self.step_count}.png")) + + # Terminate if less than two agents + self.active_agents = self.get_active_agents() + if len(self.active_agents) < 2: + done = True + info["solved"] = True + elif done: + info["solved"] = False + + if done and self.record_video and self.recording_started: + gif_path = self.video_filename + if gif_path == "": + gif_path = "lasertag.gif" + elif gif_path.endswith("mp4"): + gif_path = gif_path[:-4] + ".gif" + # Make the GIF and delete the temporary directory + png_files = glob.glob(os.path.join(self.tmpdir, "e_s_*.png")) + png_files.sort(key=os.path.getmtime) + + img, *imgs = [PIL.Image.open(f) for f in png_files] + img.save( + fp=gif_path, + format="GIF", + append_images=imgs, + save_all=True, + duration=60, + loop=0, + ) + shutil.rmtree(self.tmpdir) + + print("Saving replay GIF at {}".format(os.path.abspath(gif_path))) + + # dead agents don't get rewarded + + if not self.zero_sum: + for a in range(self.n_agents): + if reward[a] != 0 and a not in self.active_agents: + reward[a] = 0 + + # Survival mode: re-compute all rewards + if self.survival_mode: + reward = [0] * self.n_agents + if len(self.active_agents) == 1: + active_agent_index = next(iter(self.active_agents)) + reward[active_agent_index] = 1 + + return self.update_obs(obs), reward, done, info + + def get_avail_actions(self): + """Returns the available actions of all agents in a list.""" + avail_actions = [] + for agent_id in range(self.n_agents): + avail_agent = self.get_avail_agent_actions(agent_id) + avail_actions.append(avail_agent) + return avail_actions + + def get_avail_agent_actions(self, agent_id): + if agent_id not in self.active_agents: # agent is dead + # only no_op allowed + return [1] + [0] * (self.n_actions - 1) + else: + # everything other than no_op allowed + return [0] + [1] * (self.n_actions - 1) + + def reset(self, *args, level=None, **kwargs): + self.step_count = 0 + if level is None: + obs = super().reset(*args, **kwargs) + else: + assert isinstance(level, np.ndarray) + assert len(level.shape) == 2 + shape_0 = level.shape[0] + shape_1 = level.shape[1] + + # Wallify from four sides + lvl = np.full((shape_0 + 2, shape_1 + 2), "w", dtype="U5") + lvl[1 : shape_0 + 1, 1 : shape_1 + 1] = level + + # translate into a string + level_list = ["\t".join(lvl[i]) + "\n" for i in range(lvl.shape[0])] + obs = super().reset(*args, level_string="".join(level_list), **kwargs) + + self.active_agents = self.get_active_agents() + + if self.record_video and not self.recording_started: + frame = self.render(mode="rgb_array") + # creating gifs not videos + self.tmpdir = tempfile.mkdtemp() + image = PIL.Image.fromarray(frame) + image.save(os.path.join(self.tmpdir, f"e_s_{self.step_count}.png")) + self.recording_started = True + + return self.update_obs(obs) + + def update_obs(self, obs): + obs_dict = {} + obs_dict["image"] = np.stack(obs, axis=0) + if self.mask_actions: + obs_dict["avail_actions"] = np.stack(self.get_avail_actions(), axis=0) + # Zero out obs of dead agents + for a in range(self.player_count): + if a not in self.active_agents: + obs_dict["image"][a, :, :, :] = 0 + + return obs_dict + + def seed(self, seed=None): + # super().seed(seed=seed) + np.random.seed(seed) + return seed + + def render(self, mode=None): + return super().render(observer="global", mode="rgb_array") diff --git a/lasertag/lasertag_wall_general_zs.yaml b/lasertag/lasertag_wall_general_zs.yaml new file mode 100644 index 00000000..8983587d --- /dev/null +++ b/lasertag/lasertag_wall_general_zs.yaml @@ -0,0 +1,194 @@ +Version: "0.1" +Environment: + Name: Laser Tag + Observers: + Sprite2D: + TileSize: 24 + BackgroundTile: Passage.png + Block2D: + TileSize: 32 + BackgroundColor: [0.0,0.0,0.0] + Shader: + ObserverAvatarMode: HIGHLIGHT + # ObserverAvatarHighlightColor: [0.1, 0.2, 0.35] + ObserverAvatarHighlightColor: [0.3, 0.3, 0.3] + Vector: + IncludePlayerID: false + IncludeRotation: false + IncludeVariables: false + Player: + Colors: + - [1.0, 0.2, 0.4] + - [0.27, 0.69, 0.99] + - [0.0, 1.0, 0.0] + Count: $N_AGENT + AvatarObject: agent # TODO set this programmatically + Observer: + HighlightPlayers: false # Add highlights to the players in visual observers. + RotateWithAvatar: true # rotate to follow the orientation of the avatar. + TrackAvatar: true # The observer view will track the position of the avatar + Height: 5 + Width: 5 + OffsetX: 0 + OffsetY: 2 + Variables: + - Name: dummy + InitialValue: 1 + Termination: + End: + - eq: [ dummy, 0 ] # make sure episode doesn't terminate + Levels: + - | + . . . . . . . . a2 + . . . . . . . . . + . . . w w w w . . + . . . . . w w . . + a1 . . w . . w . . + . . . w w . . . . + . . . w w w w . w + . . . . . . . . . + . . . . w . . . . + +Actions: + + - Name: flame_projectile_movement + InputMapping: + Internal: true + Behaviours: + - Src: + Object: flame + Commands: + - mov: _dest + - eq: + Arguments: [ range, 0 ] + Commands: + - remove: true + - gt: + Arguments: [ range, 0 ] + Commands: + - decr: range + - exec: + Action: flame_projectile_movement + Delay: 1 + Dst: + Object: _empty + - Src: # Flame -> wall + Object: flame + Commands: + - remove: true + Dst: + Object: wall + # Commands: + # - remove: true + - Src: # Flame -> Agent + Object: flame + Commands: + - remove: true + - reward: 1 + Dst: + Object: agent + Commands: + - remove: true + - reward: -1 + - Src: # Flame -> [Flame, Border] + Object: flame + Commands: + - remove: true + Dst: + Object: [flame, _boundary] + + - Name: stumble + InputMapping: + Inputs: + 1: + Description: Rotate left + OrientationVector: [-1, 0] + 2: + Description: Move forwards + OrientationVector: [0, -1] + VectorToDest: [0, -1] + 3: + Description: Rotate right + OrientationVector: [1, 0] + Relative: true + Behaviours: + # Tell the agent to rotate if the agent performs an action on itself + - Src: + Object: agent + Commands: + - rot: _dir + Dst: + Object: agent + + # The agent can move around freely in empty and always rotates the direction it is travelling + - Src: + Object: agent + Commands: + - mov: _dest + Dst: + Object: _empty + + - Name: flame_shoot + InputMapping: + Inputs: + 1: # Only shoot the direction the agent is facing + OrientationVector: [ 0, -1 ] + VectorToDest: [ 0, -1 ] + Relative: true + Behaviours: + - Src: + Object: agent + Dst: + Object: _empty + Commands: + - spawn: flame + + +Objects: + - Name: wall + Z: 2 + MapCharacter: w + Observers: + Isometric: + - Image: oryx/oryx_iso_dungeon/crate-1.png + Sprite2D: + - Image: Rigid.png + Block2D: + - Shape: square + Color: [0.7, 0.7, 0.7] + Scale: 1.0 + + - Name: agent + Z: 2 + MapCharacter: a + Observers: + Isometric: + - Image: oryx/oryx_iso_dungeon/jelly-1.png + Sprite2D: + - Image: Agent0.png + Block2D: + - Shape: triangle + Color: PLAYER + Scale: 1.0 + + - Name: flame + Z: 2 + InitialActions: + - Action: flame_projectile_movement + Delay: 2 + Variables: + - Name: range + InitialValue: 60 + MapCharacter: f + Observers: + Isometric: + - Image: oryx/oryx_iso_dungeon/fire-pink-up.png + - Image: oryx/oryx_iso_dungeon/fire-pink-right.png + - Image: oryx/oryx_iso_dungeon/fire-pink-down.png + - Image: oryx/oryx_iso_dungeon/fire-pink-left.png + Sprite2D: + - Image: Flames.png + Block2D: + - Shape: circle + Color: [1.0, 0.0, 0.0] + Scale: 0.4 \ No newline at end of file diff --git a/lasertag/poetry.lock b/lasertag/poetry.lock new file mode 100644 index 00000000..62c657f0 --- /dev/null +++ b/lasertag/poetry.lock @@ -0,0 +1,1434 @@ +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. + +[[package]] +name = "appnope" +version = "0.1.4" +description = "Disable App Nap on macOS >= 10.9" +optional = false +python-versions = ">=3.6" +files = [ + {file = "appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c"}, + {file = "appnope-0.1.4.tar.gz", hash = "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee"}, +] + +[[package]] +name = "asttokens" +version = "2.4.1" +description = "Annotate AST trees with source code positions" +optional = false +python-versions = "*" +files = [ + {file = "asttokens-2.4.1-py2.py3-none-any.whl", hash = "sha256:051ed49c3dcae8913ea7cd08e46a606dba30b79993209636c4875bc1d637bc24"}, + {file = "asttokens-2.4.1.tar.gz", hash = "sha256:b03869718ba9a6eb027e134bfdf69f38a236d681c83c160d510768af11254ba0"}, +] + +[package.dependencies] +six = ">=1.12.0" + +[package.extras] +astroid = ["astroid (>=1,<2)", "astroid (>=2,<4)"] +test = ["astroid (>=1,<2)", "astroid (>=2,<4)", "pytest"] + +[[package]] +name = "cffi" +version = "1.16.0" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = ">=3.8" +files = [ + {file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"}, + {file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"}, + {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"}, + {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"}, + {file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"}, + {file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"}, + {file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"}, + {file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"}, + {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"}, + {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"}, + {file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"}, + {file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"}, + {file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"}, + {file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"}, + {file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"}, + {file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"}, + {file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"}, + {file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"}, + {file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"}, + {file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"}, + {file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"}, + {file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"}, + {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"}, + {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"}, + {file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"}, + {file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"}, + {file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"}, +] + +[package.dependencies] +pycparser = "*" + +[[package]] +name = "cloudpickle" +version = "3.0.0" +description = "Pickler class to extend the standard pickle.Pickler functionality" +optional = false +python-versions = ">=3.8" +files = [ + {file = "cloudpickle-3.0.0-py3-none-any.whl", hash = "sha256:246ee7d0c295602a036e86369c77fecda4ab17b506496730f2f576d9016fd9c7"}, + {file = "cloudpickle-3.0.0.tar.gz", hash = "sha256:996d9a482c6fb4f33c1a35335cf8afd065d2a56e973270364840712d9131a882"}, +] + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "comm" +version = "0.2.2" +description = "Jupyter Python Comm implementation, for usage in ipykernel, xeus-python etc." +optional = false +python-versions = ">=3.8" +files = [ + {file = "comm-0.2.2-py3-none-any.whl", hash = "sha256:e6fb86cb70ff661ee8c9c14e7d36d6de3b4066f1441be4063df9c5009f0a64d3"}, + {file = "comm-0.2.2.tar.gz", hash = "sha256:3fd7a84065306e07bea1773df6eb8282de51ba82f77c72f9c85716ab11fe980e"}, +] + +[package.dependencies] +traitlets = ">=4" + +[package.extras] +test = ["pytest"] + +[[package]] +name = "debugpy" +version = "1.8.1" +description = "An implementation of the Debug Adapter Protocol for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "debugpy-1.8.1-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:3bda0f1e943d386cc7a0e71bfa59f4137909e2ed947fb3946c506e113000f741"}, + {file = "debugpy-1.8.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dda73bf69ea479c8577a0448f8c707691152e6c4de7f0c4dec5a4bc11dee516e"}, + {file = "debugpy-1.8.1-cp310-cp310-win32.whl", hash = "sha256:3a79c6f62adef994b2dbe9fc2cc9cc3864a23575b6e387339ab739873bea53d0"}, + {file = "debugpy-1.8.1-cp310-cp310-win_amd64.whl", hash = "sha256:7eb7bd2b56ea3bedb009616d9e2f64aab8fc7000d481faec3cd26c98a964bcdd"}, + {file = "debugpy-1.8.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:016a9fcfc2c6b57f939673c874310d8581d51a0fe0858e7fac4e240c5eb743cb"}, + {file = "debugpy-1.8.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd97ed11a4c7f6d042d320ce03d83b20c3fb40da892f994bc041bbc415d7a099"}, + {file = "debugpy-1.8.1-cp311-cp311-win32.whl", hash = "sha256:0de56aba8249c28a300bdb0672a9b94785074eb82eb672db66c8144fff673146"}, + {file = "debugpy-1.8.1-cp311-cp311-win_amd64.whl", hash = "sha256:1a9fe0829c2b854757b4fd0a338d93bc17249a3bf69ecf765c61d4c522bb92a8"}, + {file = "debugpy-1.8.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3ebb70ba1a6524d19fa7bb122f44b74170c447d5746a503e36adc244a20ac539"}, + {file = "debugpy-1.8.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2e658a9630f27534e63922ebf655a6ab60c370f4d2fc5c02a5b19baf4410ace"}, + {file = "debugpy-1.8.1-cp312-cp312-win32.whl", hash = "sha256:caad2846e21188797a1f17fc09c31b84c7c3c23baf2516fed5b40b378515bbf0"}, + {file = "debugpy-1.8.1-cp312-cp312-win_amd64.whl", hash = "sha256:edcc9f58ec0fd121a25bc950d4578df47428d72e1a0d66c07403b04eb93bcf98"}, + {file = "debugpy-1.8.1-cp38-cp38-macosx_11_0_x86_64.whl", hash = "sha256:7a3afa222f6fd3d9dfecd52729bc2e12c93e22a7491405a0ecbf9e1d32d45b39"}, + {file = "debugpy-1.8.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d915a18f0597ef685e88bb35e5d7ab968964b7befefe1aaea1eb5b2640b586c7"}, + {file = "debugpy-1.8.1-cp38-cp38-win32.whl", hash = "sha256:92116039b5500633cc8d44ecc187abe2dfa9b90f7a82bbf81d079fcdd506bae9"}, + {file = "debugpy-1.8.1-cp38-cp38-win_amd64.whl", hash = "sha256:e38beb7992b5afd9d5244e96ad5fa9135e94993b0c551ceebf3fe1a5d9beb234"}, + {file = "debugpy-1.8.1-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:bfb20cb57486c8e4793d41996652e5a6a885b4d9175dd369045dad59eaacea42"}, + {file = "debugpy-1.8.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efd3fdd3f67a7e576dd869c184c5dd71d9aaa36ded271939da352880c012e703"}, + {file = "debugpy-1.8.1-cp39-cp39-win32.whl", hash = "sha256:58911e8521ca0c785ac7a0539f1e77e0ce2df753f786188f382229278b4cdf23"}, + {file = "debugpy-1.8.1-cp39-cp39-win_amd64.whl", hash = "sha256:6df9aa9599eb05ca179fb0b810282255202a66835c6efb1d112d21ecb830ddd3"}, + {file = "debugpy-1.8.1-py2.py3-none-any.whl", hash = "sha256:28acbe2241222b87e255260c76741e1fbf04fdc3b6d094fcf57b6c6f75ce1242"}, + {file = "debugpy-1.8.1.zip", hash = "sha256:f696d6be15be87aef621917585f9bb94b1dc9e8aced570db1b8a6fc14e8f9b42"}, +] + +[[package]] +name = "decorator" +version = "5.1.1" +description = "Decorators for Humans" +optional = false +python-versions = ">=3.5" +files = [ + {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"}, + {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, +] + +[[package]] +name = "exceptiongroup" +version = "1.2.0" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"}, + {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "executing" +version = "2.0.1" +description = "Get the currently executing AST node of a frame, and other information" +optional = false +python-versions = ">=3.5" +files = [ + {file = "executing-2.0.1-py2.py3-none-any.whl", hash = "sha256:eac49ca94516ccc753f9fb5ce82603156e590b27525a8bc32cce8ae302eb61bc"}, + {file = "executing-2.0.1.tar.gz", hash = "sha256:35afe2ce3affba8ee97f2d69927fa823b08b472b7b994e36a52a964b93d16147"}, +] + +[package.extras] +tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipython", "littleutils", "pytest", "rich"] + +[[package]] +name = "filelock" +version = "3.13.1" +description = "A platform independent file lock." +optional = false +python-versions = ">=3.8" +files = [ + {file = "filelock-3.13.1-py3-none-any.whl", hash = "sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c"}, + {file = "filelock-3.13.1.tar.gz", hash = "sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e"}, +] + +[package.extras] +docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.24)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] +typing = ["typing-extensions (>=4.8)"] + +[[package]] +name = "fsspec" +version = "2024.3.1" +description = "File-system specification" +optional = false +python-versions = ">=3.8" +files = [ + {file = "fsspec-2024.3.1-py3-none-any.whl", hash = "sha256:918d18d41bf73f0e2b261824baeb1b124bcf771767e3a26425cd7dec3332f512"}, + {file = "fsspec-2024.3.1.tar.gz", hash = "sha256:f39780e282d7d117ffb42bb96992f8a90795e4d0fb0f661a70ca39fe9c43ded9"}, +] + +[package.extras] +abfs = ["adlfs"] +adl = ["adlfs"] +arrow = ["pyarrow (>=1)"] +dask = ["dask", "distributed"] +devel = ["pytest", "pytest-cov"] +dropbox = ["dropbox", "dropboxdrivefs", "requests"] +full = ["adlfs", "aiohttp (!=4.0.0a0,!=4.0.0a1)", "dask", "distributed", "dropbox", "dropboxdrivefs", "fusepy", "gcsfs", "libarchive-c", "ocifs", "panel", "paramiko", "pyarrow (>=1)", "pygit2", "requests", "s3fs", "smbprotocol", "tqdm"] +fuse = ["fusepy"] +gcs = ["gcsfs"] +git = ["pygit2"] +github = ["requests"] +gs = ["gcsfs"] +gui = ["panel"] +hdfs = ["pyarrow (>=1)"] +http = ["aiohttp (!=4.0.0a0,!=4.0.0a1)"] +libarchive = ["libarchive-c"] +oci = ["ocifs"] +s3 = ["s3fs"] +sftp = ["paramiko"] +smb = ["smbprotocol"] +ssh = ["paramiko"] +tqdm = ["tqdm"] + +[[package]] +name = "griddly" +version = "1.6.7" +description = "Griddly Python Libraries" +optional = false +python-versions = "*" +files = [ + {file = "griddly-1.6.7-cp310-cp310-manylinux2014_x86_64.whl", hash = "sha256:7f45a52a5ca17276cccfb012532431caf50677f15c1b185cce1faa757bc0e7e7"}, + {file = "griddly-1.6.7-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:916841daaf35f15f6dbad01824c89bc71117a0756e5477ef0f9a76391afc02cd"}, + {file = "griddly-1.6.7-cp310-cp310-win_amd64.whl", hash = "sha256:8df04e599f2fcc20e31d01d1e3a68c224508b903b13610e9d23dbf41ce07002e"}, + {file = "griddly-1.6.7-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:4c153f1b18c3e7493cd7da140a92c79a05fa15a664b2ced32f663af87d0851cf"}, + {file = "griddly-1.6.7-cp311-cp311-win_amd64.whl", hash = "sha256:faa186c28099c164a57fede1ac0b1de0e173b4a19faa287b23ab8b9febad7a85"}, + {file = "griddly-1.6.7-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:4c6dd76ca8af80932770edcd55ca47c862968a0bfe994315c31f2a24d34c7927"}, + {file = "griddly-1.6.7-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:a2dc2a876d279ba3d76cd93b2360c9de8a058705729ad766761e760f8cd6d4ad"}, + {file = "griddly-1.6.7-cp38-cp38-win_amd64.whl", hash = "sha256:3a358ccf71e3e6301ce72980d58dff4d65291113919355f82b365ccd82cb4543"}, + {file = "griddly-1.6.7-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:d6bff9571ec547a32ecd7951206b7fd103135f1213b3d9c9f293f9d0b4668d6b"}, + {file = "griddly-1.6.7-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:0fa5c7fd123cd82787f1356012db5f22c6f579a7f4b4f031343cdae50c5a18d7"}, + {file = "griddly-1.6.7-cp39-cp39-win_amd64.whl", hash = "sha256:310c9e20e9c4a62f7ac60bc7af8e103fabc32dc725e5f2053a1047389659e592"}, +] + +[package.dependencies] +gym = ">=0.17.3" +imageio = ">=2.9.0" +numpy = ">=1.20.3" +pyyaml = ">=5.3.1" + +[[package]] +name = "gym" +version = "0.26.2" +description = "Gym: A universal API for reinforcement learning environments" +optional = false +python-versions = ">=3.6" +files = [ + {file = "gym-0.26.2.tar.gz", hash = "sha256:e0d882f4b54f0c65f203104c24ab8a38b039f1289986803c7d02cdbe214fbcc4"}, +] + +[package.dependencies] +cloudpickle = ">=1.2.0" +gym_notices = ">=0.0.4" +numpy = ">=1.18.0" + +[package.extras] +accept-rom-license = ["autorom[accept-rom-license] (>=0.4.2,<0.5.0)"] +all = ["ale-py (>=0.8.0,<0.9.0)", "box2d-py (==2.3.5)", "imageio (>=2.14.1)", "lz4 (>=3.1.0)", "matplotlib (>=3.0)", "moviepy (>=1.0.0)", "mujoco (==2.2)", "mujoco_py (>=2.1,<2.2)", "opencv-python (>=3.0)", "pygame (==2.1.0)", "pytest (==7.0.1)", "swig (==4.*)"] +atari = ["ale-py (>=0.8.0,<0.9.0)"] +box2d = ["box2d-py (==2.3.5)", "pygame (==2.1.0)", "swig (==4.*)"] +classic-control = ["pygame (==2.1.0)"] +mujoco = ["imageio (>=2.14.1)", "mujoco (==2.2)"] +mujoco-py = ["mujoco_py (>=2.1,<2.2)"] +other = ["lz4 (>=3.1.0)", "matplotlib (>=3.0)", "moviepy (>=1.0.0)", "opencv-python (>=3.0)"] +testing = ["box2d-py (==2.3.5)", "imageio (>=2.14.1)", "lz4 (>=3.1.0)", "matplotlib (>=3.0)", "moviepy (>=1.0.0)", "mujoco (==2.2)", "mujoco_py (>=2.1,<2.2)", "opencv-python (>=3.0)", "pygame (==2.1.0)", "pytest (==7.0.1)", "swig (==4.*)"] +toy-text = ["pygame (==2.1.0)"] + +[[package]] +name = "gym-notices" +version = "0.0.8" +description = "Notices for gym" +optional = false +python-versions = "*" +files = [ + {file = "gym-notices-0.0.8.tar.gz", hash = "sha256:ad25e200487cafa369728625fe064e88ada1346618526102659b4640f2b4b911"}, + {file = "gym_notices-0.0.8-py3-none-any.whl", hash = "sha256:e5f82e00823a166747b4c2a07de63b6560b1acb880638547e0cabf825a01e463"}, +] + +[[package]] +name = "imageio" +version = "2.34.0" +description = "Library for reading and writing a wide range of image, video, scientific, and volumetric data formats." +optional = false +python-versions = ">=3.8" +files = [ + {file = "imageio-2.34.0-py3-none-any.whl", hash = "sha256:08082bf47ccb54843d9c73fe9fc8f3a88c72452ab676b58aca74f36167e8ccba"}, + {file = "imageio-2.34.0.tar.gz", hash = "sha256:ae9732e10acf807a22c389aef193f42215718e16bd06eed0c5bb57e1034a4d53"}, +] + +[package.dependencies] +numpy = "*" +pillow = ">=8.3.2" + +[package.extras] +all-plugins = ["astropy", "av", "imageio-ffmpeg", "pillow-heif", "psutil", "tifffile"] +all-plugins-pypy = ["av", "imageio-ffmpeg", "pillow-heif", "psutil", "tifffile"] +build = ["wheel"] +dev = ["black", "flake8", "fsspec[github]", "pytest", "pytest-cov"] +docs = ["numpydoc", "pydata-sphinx-theme", "sphinx (<6)"] +ffmpeg = ["imageio-ffmpeg", "psutil"] +fits = ["astropy"] +full = ["astropy", "av", "black", "flake8", "fsspec[github]", "gdal", "imageio-ffmpeg", "itk", "numpydoc", "pillow-heif", "psutil", "pydata-sphinx-theme", "pytest", "pytest-cov", "sphinx (<6)", "tifffile", "wheel"] +gdal = ["gdal"] +itk = ["itk"] +linting = ["black", "flake8"] +pillow-heif = ["pillow-heif"] +pyav = ["av"] +test = ["fsspec[github]", "pytest", "pytest-cov"] +tifffile = ["tifffile"] + +[[package]] +name = "ipykernel" +version = "6.29.3" +description = "IPython Kernel for Jupyter" +optional = false +python-versions = ">=3.8" +files = [ + {file = "ipykernel-6.29.3-py3-none-any.whl", hash = "sha256:5aa086a4175b0229d4eca211e181fb473ea78ffd9869af36ba7694c947302a21"}, + {file = "ipykernel-6.29.3.tar.gz", hash = "sha256:e14c250d1f9ea3989490225cc1a542781b095a18a19447fcf2b5eaf7d0ac5bd2"}, +] + +[package.dependencies] +appnope = {version = "*", markers = "platform_system == \"Darwin\""} +comm = ">=0.1.1" +debugpy = ">=1.6.5" +ipython = ">=7.23.1" +jupyter-client = ">=6.1.12" +jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" +matplotlib-inline = ">=0.1" +nest-asyncio = "*" +packaging = "*" +psutil = "*" +pyzmq = ">=24" +tornado = ">=6.1" +traitlets = ">=5.4.0" + +[package.extras] +cov = ["coverage[toml]", "curio", "matplotlib", "pytest-cov", "trio"] +docs = ["myst-parser", "pydata-sphinx-theme", "sphinx", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling", "trio"] +pyqt5 = ["pyqt5"] +pyside6 = ["pyside6"] +test = ["flaky", "ipyparallel", "pre-commit", "pytest (>=7.0)", "pytest-asyncio (>=0.23.5)", "pytest-cov", "pytest-timeout"] + +[[package]] +name = "ipython" +version = "8.22.2" +description = "IPython: Productive Interactive Computing" +optional = false +python-versions = ">=3.10" +files = [ + {file = "ipython-8.22.2-py3-none-any.whl", hash = "sha256:3c86f284c8f3d8f2b6c662f885c4889a91df7cd52056fd02b7d8d6195d7f56e9"}, + {file = "ipython-8.22.2.tar.gz", hash = "sha256:2dcaad9049f9056f1fef63514f176c7d41f930daa78d05b82a176202818f2c14"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +decorator = "*" +exceptiongroup = {version = "*", markers = "python_version < \"3.11\""} +jedi = ">=0.16" +matplotlib-inline = "*" +pexpect = {version = ">4.3", markers = "sys_platform != \"win32\" and sys_platform != \"emscripten\""} +prompt-toolkit = ">=3.0.41,<3.1.0" +pygments = ">=2.4.0" +stack-data = "*" +traitlets = ">=5.13.0" + +[package.extras] +all = ["ipython[black,doc,kernel,nbconvert,nbformat,notebook,parallel,qtconsole,terminal]", "ipython[test,test-extra]"] +black = ["black"] +doc = ["docrepr", "exceptiongroup", "ipykernel", "ipython[test]", "matplotlib", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "sphinxcontrib-jquery", "stack-data", "typing-extensions"] +kernel = ["ipykernel"] +nbconvert = ["nbconvert"] +nbformat = ["nbformat"] +notebook = ["ipywidgets", "notebook"] +parallel = ["ipyparallel"] +qtconsole = ["qtconsole"] +test = ["pickleshare", "pytest (<8)", "pytest-asyncio (<0.22)", "testpath"] +test-extra = ["curio", "ipython[test]", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.23)", "pandas", "trio"] + +[[package]] +name = "jedi" +version = "0.19.1" +description = "An autocompletion tool for Python that can be used for text editors." +optional = false +python-versions = ">=3.6" +files = [ + {file = "jedi-0.19.1-py2.py3-none-any.whl", hash = "sha256:e983c654fe5c02867aef4cdfce5a2fbb4a50adc0af145f70504238f18ef5e7e0"}, + {file = "jedi-0.19.1.tar.gz", hash = "sha256:cf0496f3651bc65d7174ac1b7d043eff454892c708a87d1b683e57b569927ffd"}, +] + +[package.dependencies] +parso = ">=0.8.3,<0.9.0" + +[package.extras] +docs = ["Jinja2 (==2.11.3)", "MarkupSafe (==1.1.1)", "Pygments (==2.8.1)", "alabaster (==0.7.12)", "babel (==2.9.1)", "chardet (==4.0.0)", "commonmark (==0.8.1)", "docutils (==0.17.1)", "future (==0.18.2)", "idna (==2.10)", "imagesize (==1.2.0)", "mock (==1.0.1)", "packaging (==20.9)", "pyparsing (==2.4.7)", "pytz (==2021.1)", "readthedocs-sphinx-ext (==2.1.4)", "recommonmark (==0.5.0)", "requests (==2.25.1)", "six (==1.15.0)", "snowballstemmer (==2.1.0)", "sphinx (==1.8.5)", "sphinx-rtd-theme (==0.4.3)", "sphinxcontrib-serializinghtml (==1.1.4)", "sphinxcontrib-websupport (==1.2.4)", "urllib3 (==1.26.4)"] +qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"] +testing = ["Django", "attrs", "colorama", "docopt", "pytest (<7.0.0)"] + +[[package]] +name = "jinja2" +version = "3.1.3" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +files = [ + {file = "Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa"}, + {file = "Jinja2-3.1.3.tar.gz", hash = "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "jupyter-client" +version = "8.6.1" +description = "Jupyter protocol implementation and client libraries" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jupyter_client-8.6.1-py3-none-any.whl", hash = "sha256:3b7bd22f058434e3b9a7ea4b1500ed47de2713872288c0d511d19926f99b459f"}, + {file = "jupyter_client-8.6.1.tar.gz", hash = "sha256:e842515e2bab8e19186d89fdfea7abd15e39dd581f94e399f00e2af5a1652d3f"}, +] + +[package.dependencies] +jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" +python-dateutil = ">=2.8.2" +pyzmq = ">=23.0" +tornado = ">=6.2" +traitlets = ">=5.3" + +[package.extras] +docs = ["ipykernel", "myst-parser", "pydata-sphinx-theme", "sphinx (>=4)", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling"] +test = ["coverage", "ipykernel (>=6.14)", "mypy", "paramiko", "pre-commit", "pytest", "pytest-cov", "pytest-jupyter[client] (>=0.4.1)", "pytest-timeout"] + +[[package]] +name = "jupyter-core" +version = "5.7.2" +description = "Jupyter core package. A base package on which Jupyter projects rely." +optional = false +python-versions = ">=3.8" +files = [ + {file = "jupyter_core-5.7.2-py3-none-any.whl", hash = "sha256:4f7315d2f6b4bcf2e3e7cb6e46772eba760ae459cd1f59d29eb57b0a01bd7409"}, + {file = "jupyter_core-5.7.2.tar.gz", hash = "sha256:aa5f8d32bbf6b431ac830496da7392035d6f61b4f54872f15c4bd2a9c3f536d9"}, +] + +[package.dependencies] +platformdirs = ">=2.5" +pywin32 = {version = ">=300", markers = "sys_platform == \"win32\" and platform_python_implementation != \"PyPy\""} +traitlets = ">=5.3" + +[package.extras] +docs = ["myst-parser", "pydata-sphinx-theme", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling", "traitlets"] +test = ["ipykernel", "pre-commit", "pytest (<8)", "pytest-cov", "pytest-timeout"] + +[[package]] +name = "markupsafe" +version = "2.1.5" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.7" +files = [ + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"}, + {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, +] + +[[package]] +name = "matplotlib-inline" +version = "0.1.6" +description = "Inline Matplotlib backend for Jupyter" +optional = false +python-versions = ">=3.5" +files = [ + {file = "matplotlib-inline-0.1.6.tar.gz", hash = "sha256:f887e5f10ba98e8d2b150ddcf4702c1e5f8b3a20005eb0f74bfdbd360ee6f304"}, + {file = "matplotlib_inline-0.1.6-py3-none-any.whl", hash = "sha256:f1f41aab5328aa5aaea9b16d083b128102f8712542f819fe7e6a420ff581b311"}, +] + +[package.dependencies] +traitlets = "*" + +[[package]] +name = "mpmath" +version = "1.3.0" +description = "Python library for arbitrary-precision floating-point arithmetic" +optional = false +python-versions = "*" +files = [ + {file = "mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c"}, + {file = "mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f"}, +] + +[package.extras] +develop = ["codecov", "pycodestyle", "pytest (>=4.6)", "pytest-cov", "wheel"] +docs = ["sphinx"] +gmpy = ["gmpy2 (>=2.1.0a4)"] +tests = ["pytest (>=4.6)"] + +[[package]] +name = "nest-asyncio" +version = "1.6.0" +description = "Patch asyncio to allow nested event loops" +optional = false +python-versions = ">=3.5" +files = [ + {file = "nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c"}, + {file = "nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe"}, +] + +[[package]] +name = "networkx" +version = "3.2.1" +description = "Python package for creating and manipulating graphs and networks" +optional = false +python-versions = ">=3.9" +files = [ + {file = "networkx-3.2.1-py3-none-any.whl", hash = "sha256:f18c69adc97877c42332c170849c96cefa91881c99a7cb3e95b7c659ebdc1ec2"}, + {file = "networkx-3.2.1.tar.gz", hash = "sha256:9f1bb5cf3409bf324e0a722c20bdb4c20ee39bf1c30ce8ae499c8502b0b5e0c6"}, +] + +[package.extras] +default = ["matplotlib (>=3.5)", "numpy (>=1.22)", "pandas (>=1.4)", "scipy (>=1.9,!=1.11.0,!=1.11.1)"] +developer = ["changelist (==0.4)", "mypy (>=1.1)", "pre-commit (>=3.2)", "rtoml"] +doc = ["nb2plots (>=0.7)", "nbconvert (<7.9)", "numpydoc (>=1.6)", "pillow (>=9.4)", "pydata-sphinx-theme (>=0.14)", "sphinx (>=7)", "sphinx-gallery (>=0.14)", "texext (>=0.6.7)"] +extra = ["lxml (>=4.6)", "pydot (>=1.4.2)", "pygraphviz (>=1.11)", "sympy (>=1.10)"] +test = ["pytest (>=7.2)", "pytest-cov (>=4.0)"] + +[[package]] +name = "numpy" +version = "1.26.4" +description = "Fundamental package for array computing in Python" +optional = false +python-versions = ">=3.9" +files = [ + {file = "numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0"}, + {file = "numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a"}, + {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4"}, + {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f"}, + {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a"}, + {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2"}, + {file = "numpy-1.26.4-cp310-cp310-win32.whl", hash = "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07"}, + {file = "numpy-1.26.4-cp310-cp310-win_amd64.whl", hash = "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5"}, + {file = "numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71"}, + {file = "numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef"}, + {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e"}, + {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5"}, + {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a"}, + {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a"}, + {file = "numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20"}, + {file = "numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2"}, + {file = "numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218"}, + {file = "numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b"}, + {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b"}, + {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed"}, + {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a"}, + {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0"}, + {file = "numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110"}, + {file = "numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818"}, + {file = "numpy-1.26.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7349ab0fa0c429c82442a27a9673fc802ffdb7c7775fad780226cb234965e53c"}, + {file = "numpy-1.26.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:52b8b60467cd7dd1e9ed082188b4e6bb35aa5cdd01777621a1658910745b90be"}, + {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5241e0a80d808d70546c697135da2c613f30e28251ff8307eb72ba696945764"}, + {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3"}, + {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:679b0076f67ecc0138fd2ede3a8fd196dddc2ad3254069bcb9faf9a79b1cebcd"}, + {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:47711010ad8555514b434df65f7d7b076bb8261df1ca9bb78f53d3b2db02e95c"}, + {file = "numpy-1.26.4-cp39-cp39-win32.whl", hash = "sha256:a354325ee03388678242a4d7ebcd08b5c727033fcff3b2f536aea978e15ee9e6"}, + {file = "numpy-1.26.4-cp39-cp39-win_amd64.whl", hash = "sha256:3373d5d70a5fe74a2c1bb6d2cfd9609ecf686d47a2d7b1d37a8f3b6bf6003aea"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:afedb719a9dcfc7eaf2287b839d8198e06dcd4cb5d276a3df279231138e83d30"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95a7476c59002f2f6c590b9b7b998306fba6a5aa646b1e22ddfeaf8f78c3a29c"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0"}, + {file = "numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010"}, +] + +[[package]] +name = "nvidia-cublas-cu12" +version = "12.1.3.1" +description = "CUBLAS native runtime libraries" +optional = false +python-versions = ">=3" +files = [ + {file = "nvidia_cublas_cu12-12.1.3.1-py3-none-manylinux1_x86_64.whl", hash = "sha256:ee53ccca76a6fc08fb9701aa95b6ceb242cdaab118c3bb152af4e579af792728"}, + {file = "nvidia_cublas_cu12-12.1.3.1-py3-none-win_amd64.whl", hash = "sha256:2b964d60e8cf11b5e1073d179d85fa340c120e99b3067558f3cf98dd69d02906"}, +] + +[[package]] +name = "nvidia-cuda-cupti-cu12" +version = "12.1.105" +description = "CUDA profiling tools runtime libs." +optional = false +python-versions = ">=3" +files = [ + {file = "nvidia_cuda_cupti_cu12-12.1.105-py3-none-manylinux1_x86_64.whl", hash = "sha256:e54fde3983165c624cb79254ae9818a456eb6e87a7fd4d56a2352c24ee542d7e"}, + {file = "nvidia_cuda_cupti_cu12-12.1.105-py3-none-win_amd64.whl", hash = "sha256:bea8236d13a0ac7190bd2919c3e8e6ce1e402104276e6f9694479e48bb0eb2a4"}, +] + +[[package]] +name = "nvidia-cuda-nvrtc-cu12" +version = "12.1.105" +description = "NVRTC native runtime libraries" +optional = false +python-versions = ">=3" +files = [ + {file = "nvidia_cuda_nvrtc_cu12-12.1.105-py3-none-manylinux1_x86_64.whl", hash = "sha256:339b385f50c309763ca65456ec75e17bbefcbbf2893f462cb8b90584cd27a1c2"}, + {file = "nvidia_cuda_nvrtc_cu12-12.1.105-py3-none-win_amd64.whl", hash = "sha256:0a98a522d9ff138b96c010a65e145dc1b4850e9ecb75a0172371793752fd46ed"}, +] + +[[package]] +name = "nvidia-cuda-runtime-cu12" +version = "12.1.105" +description = "CUDA Runtime native Libraries" +optional = false +python-versions = ">=3" +files = [ + {file = "nvidia_cuda_runtime_cu12-12.1.105-py3-none-manylinux1_x86_64.whl", hash = "sha256:6e258468ddf5796e25f1dc591a31029fa317d97a0a94ed93468fc86301d61e40"}, + {file = "nvidia_cuda_runtime_cu12-12.1.105-py3-none-win_amd64.whl", hash = "sha256:dfb46ef84d73fababab44cf03e3b83f80700d27ca300e537f85f636fac474344"}, +] + +[[package]] +name = "nvidia-cudnn-cu12" +version = "8.9.2.26" +description = "cuDNN runtime libraries" +optional = false +python-versions = ">=3" +files = [ + {file = "nvidia_cudnn_cu12-8.9.2.26-py3-none-manylinux1_x86_64.whl", hash = "sha256:5ccb288774fdfb07a7e7025ffec286971c06d8d7b4fb162525334616d7629ff9"}, +] + +[package.dependencies] +nvidia-cublas-cu12 = "*" + +[[package]] +name = "nvidia-cufft-cu12" +version = "11.0.2.54" +description = "CUFFT native runtime libraries" +optional = false +python-versions = ">=3" +files = [ + {file = "nvidia_cufft_cu12-11.0.2.54-py3-none-manylinux1_x86_64.whl", hash = "sha256:794e3948a1aa71fd817c3775866943936774d1c14e7628c74f6f7417224cdf56"}, + {file = "nvidia_cufft_cu12-11.0.2.54-py3-none-win_amd64.whl", hash = "sha256:d9ac353f78ff89951da4af698f80870b1534ed69993f10a4cf1d96f21357e253"}, +] + +[[package]] +name = "nvidia-curand-cu12" +version = "10.3.2.106" +description = "CURAND native runtime libraries" +optional = false +python-versions = ">=3" +files = [ + {file = "nvidia_curand_cu12-10.3.2.106-py3-none-manylinux1_x86_64.whl", hash = "sha256:9d264c5036dde4e64f1de8c50ae753237c12e0b1348738169cd0f8a536c0e1e0"}, + {file = "nvidia_curand_cu12-10.3.2.106-py3-none-win_amd64.whl", hash = "sha256:75b6b0c574c0037839121317e17fd01f8a69fd2ef8e25853d826fec30bdba74a"}, +] + +[[package]] +name = "nvidia-cusolver-cu12" +version = "11.4.5.107" +description = "CUDA solver native runtime libraries" +optional = false +python-versions = ">=3" +files = [ + {file = "nvidia_cusolver_cu12-11.4.5.107-py3-none-manylinux1_x86_64.whl", hash = "sha256:8a7ec542f0412294b15072fa7dab71d31334014a69f953004ea7a118206fe0dd"}, + {file = "nvidia_cusolver_cu12-11.4.5.107-py3-none-win_amd64.whl", hash = "sha256:74e0c3a24c78612192a74fcd90dd117f1cf21dea4822e66d89e8ea80e3cd2da5"}, +] + +[package.dependencies] +nvidia-cublas-cu12 = "*" +nvidia-cusparse-cu12 = "*" +nvidia-nvjitlink-cu12 = "*" + +[[package]] +name = "nvidia-cusparse-cu12" +version = "12.1.0.106" +description = "CUSPARSE native runtime libraries" +optional = false +python-versions = ">=3" +files = [ + {file = "nvidia_cusparse_cu12-12.1.0.106-py3-none-manylinux1_x86_64.whl", hash = "sha256:f3b50f42cf363f86ab21f720998517a659a48131e8d538dc02f8768237bd884c"}, + {file = "nvidia_cusparse_cu12-12.1.0.106-py3-none-win_amd64.whl", hash = "sha256:b798237e81b9719373e8fae8d4f091b70a0cf09d9d85c95a557e11df2d8e9a5a"}, +] + +[package.dependencies] +nvidia-nvjitlink-cu12 = "*" + +[[package]] +name = "nvidia-nccl-cu12" +version = "2.19.3" +description = "NVIDIA Collective Communication Library (NCCL) Runtime" +optional = false +python-versions = ">=3" +files = [ + {file = "nvidia_nccl_cu12-2.19.3-py3-none-manylinux1_x86_64.whl", hash = "sha256:a9734707a2c96443331c1e48c717024aa6678a0e2a4cb66b2c364d18cee6b48d"}, +] + +[[package]] +name = "nvidia-nvjitlink-cu12" +version = "12.4.99" +description = "Nvidia JIT LTO Library" +optional = false +python-versions = ">=3" +files = [ + {file = "nvidia_nvjitlink_cu12-12.4.99-py3-none-manylinux2014_aarch64.whl", hash = "sha256:75d6498c96d9adb9435f2bbdbddb479805ddfb97b5c1b32395c694185c20ca57"}, + {file = "nvidia_nvjitlink_cu12-12.4.99-py3-none-manylinux2014_x86_64.whl", hash = "sha256:c6428836d20fe7e327191c175791d38570e10762edc588fb46749217cd444c74"}, + {file = "nvidia_nvjitlink_cu12-12.4.99-py3-none-win_amd64.whl", hash = "sha256:991905ffa2144cb603d8ca7962d75c35334ae82bf92820b6ba78157277da1ad2"}, +] + +[[package]] +name = "nvidia-nvtx-cu12" +version = "12.1.105" +description = "NVIDIA Tools Extension" +optional = false +python-versions = ">=3" +files = [ + {file = "nvidia_nvtx_cu12-12.1.105-py3-none-manylinux1_x86_64.whl", hash = "sha256:dc21cf308ca5691e7c04d962e213f8a4aa9bbfa23d95412f452254c2caeb09e5"}, + {file = "nvidia_nvtx_cu12-12.1.105-py3-none-win_amd64.whl", hash = "sha256:65f4d98982b31b60026e0e6de73fbdfc09d08a96f4656dd3665ca616a11e1e82"}, +] + +[[package]] +name = "packaging" +version = "24.0" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, + {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, +] + +[[package]] +name = "parso" +version = "0.8.3" +description = "A Python Parser" +optional = false +python-versions = ">=3.6" +files = [ + {file = "parso-0.8.3-py2.py3-none-any.whl", hash = "sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75"}, + {file = "parso-0.8.3.tar.gz", hash = "sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0"}, +] + +[package.extras] +qa = ["flake8 (==3.8.3)", "mypy (==0.782)"] +testing = ["docopt", "pytest (<6.0.0)"] + +[[package]] +name = "pexpect" +version = "4.9.0" +description = "Pexpect allows easy control of interactive console applications." +optional = false +python-versions = "*" +files = [ + {file = "pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523"}, + {file = "pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f"}, +] + +[package.dependencies] +ptyprocess = ">=0.5" + +[[package]] +name = "pillow" +version = "10.2.0" +description = "Python Imaging Library (Fork)" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pillow-10.2.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:7823bdd049099efa16e4246bdf15e5a13dbb18a51b68fa06d6c1d4d8b99a796e"}, + {file = "pillow-10.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:83b2021f2ade7d1ed556bc50a399127d7fb245e725aa0113ebd05cfe88aaf588"}, + {file = "pillow-10.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fad5ff2f13d69b7e74ce5b4ecd12cc0ec530fcee76356cac6742785ff71c452"}, + {file = "pillow-10.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da2b52b37dad6d9ec64e653637a096905b258d2fc2b984c41ae7d08b938a67e4"}, + {file = "pillow-10.2.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:47c0995fc4e7f79b5cfcab1fc437ff2890b770440f7696a3ba065ee0fd496563"}, + {file = "pillow-10.2.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:322bdf3c9b556e9ffb18f93462e5f749d3444ce081290352c6070d014c93feb2"}, + {file = "pillow-10.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:51f1a1bffc50e2e9492e87d8e09a17c5eea8409cda8d3f277eb6edc82813c17c"}, + {file = "pillow-10.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:69ffdd6120a4737710a9eee73e1d2e37db89b620f702754b8f6e62594471dee0"}, + {file = "pillow-10.2.0-cp310-cp310-win32.whl", hash = "sha256:c6dafac9e0f2b3c78df97e79af707cdc5ef8e88208d686a4847bab8266870023"}, + {file = "pillow-10.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:aebb6044806f2e16ecc07b2a2637ee1ef67a11840a66752751714a0d924adf72"}, + {file = "pillow-10.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:7049e301399273a0136ff39b84c3678e314f2158f50f517bc50285fb5ec847ad"}, + {file = "pillow-10.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:35bb52c37f256f662abdfa49d2dfa6ce5d93281d323a9af377a120e89a9eafb5"}, + {file = "pillow-10.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c23f307202661071d94b5e384e1e1dc7dfb972a28a2310e4ee16103e66ddb67"}, + {file = "pillow-10.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:773efe0603db30c281521a7c0214cad7836c03b8ccff897beae9b47c0b657d61"}, + {file = "pillow-10.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11fa2e5984b949b0dd6d7a94d967743d87c577ff0b83392f17cb3990d0d2fd6e"}, + {file = "pillow-10.2.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:716d30ed977be8b37d3ef185fecb9e5a1d62d110dfbdcd1e2a122ab46fddb03f"}, + {file = "pillow-10.2.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a086c2af425c5f62a65e12fbf385f7c9fcb8f107d0849dba5839461a129cf311"}, + {file = "pillow-10.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c8de2789052ed501dd829e9cae8d3dcce7acb4777ea4a479c14521c942d395b1"}, + {file = "pillow-10.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:609448742444d9290fd687940ac0b57fb35e6fd92bdb65386e08e99af60bf757"}, + {file = "pillow-10.2.0-cp311-cp311-win32.whl", hash = "sha256:823ef7a27cf86df6597fa0671066c1b596f69eba53efa3d1e1cb8b30f3533068"}, + {file = "pillow-10.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:1da3b2703afd040cf65ec97efea81cfba59cdbed9c11d8efc5ab09df9509fc56"}, + {file = "pillow-10.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:edca80cbfb2b68d7b56930b84a0e45ae1694aeba0541f798e908a49d66b837f1"}, + {file = "pillow-10.2.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:1b5e1b74d1bd1b78bc3477528919414874748dd363e6272efd5abf7654e68bef"}, + {file = "pillow-10.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0eae2073305f451d8ecacb5474997c08569fb4eb4ac231ffa4ad7d342fdc25ac"}, + {file = "pillow-10.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7c2286c23cd350b80d2fc9d424fc797575fb16f854b831d16fd47ceec078f2c"}, + {file = "pillow-10.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e23412b5c41e58cec602f1135c57dfcf15482013ce6e5f093a86db69646a5aa"}, + {file = "pillow-10.2.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:52a50aa3fb3acb9cf7213573ef55d31d6eca37f5709c69e6858fe3bc04a5c2a2"}, + {file = "pillow-10.2.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:127cee571038f252a552760076407f9cff79761c3d436a12af6000cd182a9d04"}, + {file = "pillow-10.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:8d12251f02d69d8310b046e82572ed486685c38f02176bd08baf216746eb947f"}, + {file = "pillow-10.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:54f1852cd531aa981bc0965b7d609f5f6cc8ce8c41b1139f6ed6b3c54ab82bfb"}, + {file = "pillow-10.2.0-cp312-cp312-win32.whl", hash = "sha256:257d8788df5ca62c980314053197f4d46eefedf4e6175bc9412f14412ec4ea2f"}, + {file = "pillow-10.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:154e939c5f0053a383de4fd3d3da48d9427a7e985f58af8e94d0b3c9fcfcf4f9"}, + {file = "pillow-10.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:f379abd2f1e3dddb2b61bc67977a6b5a0a3f7485538bcc6f39ec76163891ee48"}, + {file = "pillow-10.2.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:8373c6c251f7ef8bda6675dd6d2b3a0fcc31edf1201266b5cf608b62a37407f9"}, + {file = "pillow-10.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:870ea1ada0899fd0b79643990809323b389d4d1d46c192f97342eeb6ee0b8483"}, + {file = "pillow-10.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4b6b1e20608493548b1f32bce8cca185bf0480983890403d3b8753e44077129"}, + {file = "pillow-10.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3031709084b6e7852d00479fd1d310b07d0ba82765f973b543c8af5061cf990e"}, + {file = "pillow-10.2.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:3ff074fc97dd4e80543a3e91f69d58889baf2002b6be64347ea8cf5533188213"}, + {file = "pillow-10.2.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:cb4c38abeef13c61d6916f264d4845fab99d7b711be96c326b84df9e3e0ff62d"}, + {file = "pillow-10.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b1b3020d90c2d8e1dae29cf3ce54f8094f7938460fb5ce8bc5c01450b01fbaf6"}, + {file = "pillow-10.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:170aeb00224ab3dc54230c797f8404507240dd868cf52066f66a41b33169bdbe"}, + {file = "pillow-10.2.0-cp38-cp38-win32.whl", hash = "sha256:c4225f5220f46b2fde568c74fca27ae9771536c2e29d7c04f4fb62c83275ac4e"}, + {file = "pillow-10.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:0689b5a8c5288bc0504d9fcee48f61a6a586b9b98514d7d29b840143d6734f39"}, + {file = "pillow-10.2.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:b792a349405fbc0163190fde0dc7b3fef3c9268292586cf5645598b48e63dc67"}, + {file = "pillow-10.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c570f24be1e468e3f0ce7ef56a89a60f0e05b30a3669a459e419c6eac2c35364"}, + {file = "pillow-10.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8ecd059fdaf60c1963c58ceb8997b32e9dc1b911f5da5307aab614f1ce5c2fb"}, + {file = "pillow-10.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c365fd1703040de1ec284b176d6af5abe21b427cb3a5ff68e0759e1e313a5e7e"}, + {file = "pillow-10.2.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:70c61d4c475835a19b3a5aa42492409878bbca7438554a1f89d20d58a7c75c01"}, + {file = "pillow-10.2.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:b6f491cdf80ae540738859d9766783e3b3c8e5bd37f5dfa0b76abdecc5081f13"}, + {file = "pillow-10.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9d189550615b4948f45252d7f005e53c2040cea1af5b60d6f79491a6e147eef7"}, + {file = "pillow-10.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:49d9ba1ed0ef3e061088cd1e7538a0759aab559e2e0a80a36f9fd9d8c0c21591"}, + {file = "pillow-10.2.0-cp39-cp39-win32.whl", hash = "sha256:babf5acfede515f176833ed6028754cbcd0d206f7f614ea3447d67c33be12516"}, + {file = "pillow-10.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:0304004f8067386b477d20a518b50f3fa658a28d44e4116970abfcd94fac34a8"}, + {file = "pillow-10.2.0-cp39-cp39-win_arm64.whl", hash = "sha256:0fb3e7fc88a14eacd303e90481ad983fd5b69c761e9e6ef94c983f91025da869"}, + {file = "pillow-10.2.0-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:322209c642aabdd6207517e9739c704dc9f9db943015535783239022002f054a"}, + {file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3eedd52442c0a5ff4f887fab0c1c0bb164d8635b32c894bc1faf4c618dd89df2"}, + {file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb28c753fd5eb3dd859b4ee95de66cc62af91bcff5db5f2571d32a520baf1f04"}, + {file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:33870dc4653c5017bf4c8873e5488d8f8d5f8935e2f1fb9a2208c47cdd66efd2"}, + {file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3c31822339516fb3c82d03f30e22b1d038da87ef27b6a78c9549888f8ceda39a"}, + {file = "pillow-10.2.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a2b56ba36e05f973d450582fb015594aaa78834fefe8dfb8fcd79b93e64ba4c6"}, + {file = "pillow-10.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:d8e6aeb9201e655354b3ad049cb77d19813ad4ece0df1249d3c793de3774f8c7"}, + {file = "pillow-10.2.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:2247178effb34a77c11c0e8ac355c7a741ceca0a732b27bf11e747bbc950722f"}, + {file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15587643b9e5eb26c48e49a7b33659790d28f190fc514a322d55da2fb5c2950e"}, + {file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753cd8f2086b2b80180d9b3010dd4ed147efc167c90d3bf593fe2af21265e5a5"}, + {file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:7c8f97e8e7a9009bcacbe3766a36175056c12f9a44e6e6f2d5caad06dcfbf03b"}, + {file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d1b35bcd6c5543b9cb547dee3150c93008f8dd0f1fef78fc0cd2b141c5baf58a"}, + {file = "pillow-10.2.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:fe4c15f6c9285dc54ce6553a3ce908ed37c8f3825b5a51a15c91442bb955b868"}, + {file = "pillow-10.2.0.tar.gz", hash = "sha256:e87f0b2c78157e12d7686b27d63c070fd65d994e8ddae6f328e0dcf4a0cd007e"}, +] + +[package.extras] +docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-removed-in", "sphinxext-opengraph"] +fpx = ["olefile"] +mic = ["olefile"] +tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] +typing = ["typing-extensions"] +xmp = ["defusedxml"] + +[[package]] +name = "platformdirs" +version = "4.2.0" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +optional = false +python-versions = ">=3.8" +files = [ + {file = "platformdirs-4.2.0-py3-none-any.whl", hash = "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068"}, + {file = "platformdirs-4.2.0.tar.gz", hash = "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"}, +] + +[package.extras] +docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] + +[[package]] +name = "prompt-toolkit" +version = "3.0.43" +description = "Library for building powerful interactive command lines in Python" +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "prompt_toolkit-3.0.43-py3-none-any.whl", hash = "sha256:a11a29cb3bf0a28a387fe5122cdb649816a957cd9261dcedf8c9f1fef33eacf6"}, + {file = "prompt_toolkit-3.0.43.tar.gz", hash = "sha256:3527b7af26106cbc65a040bcc84839a3566ec1b051bb0bfe953631e704b0ff7d"}, +] + +[package.dependencies] +wcwidth = "*" + +[[package]] +name = "psutil" +version = "5.9.8" +description = "Cross-platform lib for process and system monitoring in Python." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +files = [ + {file = "psutil-5.9.8-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:26bd09967ae00920df88e0352a91cff1a78f8d69b3ecabbfe733610c0af486c8"}, + {file = "psutil-5.9.8-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:05806de88103b25903dff19bb6692bd2e714ccf9e668d050d144012055cbca73"}, + {file = "psutil-5.9.8-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:611052c4bc70432ec770d5d54f64206aa7203a101ec273a0cd82418c86503bb7"}, + {file = "psutil-5.9.8-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:50187900d73c1381ba1454cf40308c2bf6f34268518b3f36a9b663ca87e65e36"}, + {file = "psutil-5.9.8-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:02615ed8c5ea222323408ceba16c60e99c3f91639b07da6373fb7e6539abc56d"}, + {file = "psutil-5.9.8-cp27-none-win32.whl", hash = "sha256:36f435891adb138ed3c9e58c6af3e2e6ca9ac2f365efe1f9cfef2794e6c93b4e"}, + {file = "psutil-5.9.8-cp27-none-win_amd64.whl", hash = "sha256:bd1184ceb3f87651a67b2708d4c3338e9b10c5df903f2e3776b62303b26cb631"}, + {file = "psutil-5.9.8-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:aee678c8720623dc456fa20659af736241f575d79429a0e5e9cf88ae0605cc81"}, + {file = "psutil-5.9.8-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cb6403ce6d8e047495a701dc7c5bd788add903f8986d523e3e20b98b733e421"}, + {file = "psutil-5.9.8-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d06016f7f8625a1825ba3732081d77c94589dca78b7a3fc072194851e88461a4"}, + {file = "psutil-5.9.8-cp36-cp36m-win32.whl", hash = "sha256:7d79560ad97af658a0f6adfef8b834b53f64746d45b403f225b85c5c2c140eee"}, + {file = "psutil-5.9.8-cp36-cp36m-win_amd64.whl", hash = "sha256:27cc40c3493bb10de1be4b3f07cae4c010ce715290a5be22b98493509c6299e2"}, + {file = "psutil-5.9.8-cp37-abi3-win32.whl", hash = "sha256:bc56c2a1b0d15aa3eaa5a60c9f3f8e3e565303b465dbf57a1b730e7a2b9844e0"}, + {file = "psutil-5.9.8-cp37-abi3-win_amd64.whl", hash = "sha256:8db4c1b57507eef143a15a6884ca10f7c73876cdf5d51e713151c1236a0e68cf"}, + {file = "psutil-5.9.8-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:d16bbddf0693323b8c6123dd804100241da461e41d6e332fb0ba6058f630f8c8"}, + {file = "psutil-5.9.8.tar.gz", hash = "sha256:6be126e3225486dff286a8fb9a06246a5253f4c7c53b475ea5f5ac934e64194c"}, +] + +[package.extras] +test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"] + +[[package]] +name = "ptyprocess" +version = "0.7.0" +description = "Run a subprocess in a pseudo terminal" +optional = false +python-versions = "*" +files = [ + {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, + {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, +] + +[[package]] +name = "pure-eval" +version = "0.2.2" +description = "Safely evaluate AST nodes without side effects" +optional = false +python-versions = "*" +files = [ + {file = "pure_eval-0.2.2-py3-none-any.whl", hash = "sha256:01eaab343580944bc56080ebe0a674b39ec44a945e6d09ba7db3cb8cec289350"}, + {file = "pure_eval-0.2.2.tar.gz", hash = "sha256:2b45320af6dfaa1750f543d714b6d1c520a1688dec6fd24d339063ce0aaa9ac3"}, +] + +[package.extras] +tests = ["pytest"] + +[[package]] +name = "pycparser" +version = "2.21" +description = "C parser in Python" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, + {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, +] + +[[package]] +name = "pygments" +version = "2.17.2" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c"}, + {file = "pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367"}, +] + +[package.extras] +plugins = ["importlib-metadata"] +windows-terminal = ["colorama (>=0.4.6)"] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "pywin32" +version = "306" +description = "Python for Window Extensions" +optional = false +python-versions = "*" +files = [ + {file = "pywin32-306-cp310-cp310-win32.whl", hash = "sha256:06d3420a5155ba65f0b72f2699b5bacf3109f36acbe8923765c22938a69dfc8d"}, + {file = "pywin32-306-cp310-cp310-win_amd64.whl", hash = "sha256:84f4471dbca1887ea3803d8848a1616429ac94a4a8d05f4bc9c5dcfd42ca99c8"}, + {file = "pywin32-306-cp311-cp311-win32.whl", hash = "sha256:e65028133d15b64d2ed8f06dd9fbc268352478d4f9289e69c190ecd6818b6407"}, + {file = "pywin32-306-cp311-cp311-win_amd64.whl", hash = "sha256:a7639f51c184c0272e93f244eb24dafca9b1855707d94c192d4a0b4c01e1100e"}, + {file = "pywin32-306-cp311-cp311-win_arm64.whl", hash = "sha256:70dba0c913d19f942a2db25217d9a1b726c278f483a919f1abfed79c9cf64d3a"}, + {file = "pywin32-306-cp312-cp312-win32.whl", hash = "sha256:383229d515657f4e3ed1343da8be101000562bf514591ff383ae940cad65458b"}, + {file = "pywin32-306-cp312-cp312-win_amd64.whl", hash = "sha256:37257794c1ad39ee9be652da0462dc2e394c8159dfd913a8a4e8eb6fd346da0e"}, + {file = "pywin32-306-cp312-cp312-win_arm64.whl", hash = "sha256:5821ec52f6d321aa59e2db7e0a35b997de60c201943557d108af9d4ae1ec7040"}, + {file = "pywin32-306-cp37-cp37m-win32.whl", hash = "sha256:1c73ea9a0d2283d889001998059f5eaaba3b6238f767c9cf2833b13e6a685f65"}, + {file = "pywin32-306-cp37-cp37m-win_amd64.whl", hash = "sha256:72c5f621542d7bdd4fdb716227be0dd3f8565c11b280be6315b06ace35487d36"}, + {file = "pywin32-306-cp38-cp38-win32.whl", hash = "sha256:e4c092e2589b5cf0d365849e73e02c391c1349958c5ac3e9d5ccb9a28e017b3a"}, + {file = "pywin32-306-cp38-cp38-win_amd64.whl", hash = "sha256:e8ac1ae3601bee6ca9f7cb4b5363bf1c0badb935ef243c4733ff9a393b1690c0"}, + {file = "pywin32-306-cp39-cp39-win32.whl", hash = "sha256:e25fd5b485b55ac9c057f67d94bc203f3f6595078d1fb3b458c9c28b7153a802"}, + {file = "pywin32-306-cp39-cp39-win_amd64.whl", hash = "sha256:39b61c15272833b5c329a2989999dcae836b1eed650252ab1b7bfbe1d59f30f4"}, +] + +[[package]] +name = "pyyaml" +version = "6.0.1" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.6" +files = [ + {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, + {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, + {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, + {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, + {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, + {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, + {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, + {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, + {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, + {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, + {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, + {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, + {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, +] + +[[package]] +name = "pyzmq" +version = "25.1.2" +description = "Python bindings for 0MQ" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pyzmq-25.1.2-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:e624c789359f1a16f83f35e2c705d07663ff2b4d4479bad35621178d8f0f6ea4"}, + {file = "pyzmq-25.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:49151b0efece79f6a79d41a461d78535356136ee70084a1c22532fc6383f4ad0"}, + {file = "pyzmq-25.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d9a5f194cf730f2b24d6af1f833c14c10f41023da46a7f736f48b6d35061e76e"}, + {file = "pyzmq-25.1.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:faf79a302f834d9e8304fafdc11d0d042266667ac45209afa57e5efc998e3872"}, + {file = "pyzmq-25.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f51a7b4ead28d3fca8dda53216314a553b0f7a91ee8fc46a72b402a78c3e43d"}, + {file = "pyzmq-25.1.2-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:0ddd6d71d4ef17ba5a87becf7ddf01b371eaba553c603477679ae817a8d84d75"}, + {file = "pyzmq-25.1.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:246747b88917e4867e2367b005fc8eefbb4a54b7db363d6c92f89d69abfff4b6"}, + {file = "pyzmq-25.1.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:00c48ae2fd81e2a50c3485de1b9d5c7c57cd85dc8ec55683eac16846e57ac979"}, + {file = "pyzmq-25.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5a68d491fc20762b630e5db2191dd07ff89834086740f70e978bb2ef2668be08"}, + {file = "pyzmq-25.1.2-cp310-cp310-win32.whl", hash = "sha256:09dfe949e83087da88c4a76767df04b22304a682d6154de2c572625c62ad6886"}, + {file = "pyzmq-25.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:fa99973d2ed20417744fca0073390ad65ce225b546febb0580358e36aa90dba6"}, + {file = "pyzmq-25.1.2-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:82544e0e2d0c1811482d37eef297020a040c32e0687c1f6fc23a75b75db8062c"}, + {file = "pyzmq-25.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:01171fc48542348cd1a360a4b6c3e7d8f46cdcf53a8d40f84db6707a6768acc1"}, + {file = "pyzmq-25.1.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc69c96735ab501419c432110016329bf0dea8898ce16fab97c6d9106dc0b348"}, + {file = "pyzmq-25.1.2-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3e124e6b1dd3dfbeb695435dff0e383256655bb18082e094a8dd1f6293114642"}, + {file = "pyzmq-25.1.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7598d2ba821caa37a0f9d54c25164a4fa351ce019d64d0b44b45540950458840"}, + {file = "pyzmq-25.1.2-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:d1299d7e964c13607efd148ca1f07dcbf27c3ab9e125d1d0ae1d580a1682399d"}, + {file = "pyzmq-25.1.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4e6f689880d5ad87918430957297c975203a082d9a036cc426648fcbedae769b"}, + {file = "pyzmq-25.1.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cc69949484171cc961e6ecd4a8911b9ce7a0d1f738fcae717177c231bf77437b"}, + {file = "pyzmq-25.1.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9880078f683466b7f567b8624bfc16cad65077be046b6e8abb53bed4eeb82dd3"}, + {file = "pyzmq-25.1.2-cp311-cp311-win32.whl", hash = "sha256:4e5837af3e5aaa99a091302df5ee001149baff06ad22b722d34e30df5f0d9097"}, + {file = "pyzmq-25.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:25c2dbb97d38b5ac9fd15586e048ec5eb1e38f3d47fe7d92167b0c77bb3584e9"}, + {file = "pyzmq-25.1.2-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:11e70516688190e9c2db14fcf93c04192b02d457b582a1f6190b154691b4c93a"}, + {file = "pyzmq-25.1.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:313c3794d650d1fccaaab2df942af9f2c01d6217c846177cfcbc693c7410839e"}, + {file = "pyzmq-25.1.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b3cbba2f47062b85fe0ef9de5b987612140a9ba3a9c6d2543c6dec9f7c2ab27"}, + {file = "pyzmq-25.1.2-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fc31baa0c32a2ca660784d5af3b9487e13b61b3032cb01a115fce6588e1bed30"}, + {file = "pyzmq-25.1.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02c9087b109070c5ab0b383079fa1b5f797f8d43e9a66c07a4b8b8bdecfd88ee"}, + {file = "pyzmq-25.1.2-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:f8429b17cbb746c3e043cb986328da023657e79d5ed258b711c06a70c2ea7537"}, + {file = "pyzmq-25.1.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:5074adeacede5f810b7ef39607ee59d94e948b4fd954495bdb072f8c54558181"}, + {file = "pyzmq-25.1.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:7ae8f354b895cbd85212da245f1a5ad8159e7840e37d78b476bb4f4c3f32a9fe"}, + {file = "pyzmq-25.1.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:b264bf2cc96b5bc43ce0e852be995e400376bd87ceb363822e2cb1964fcdc737"}, + {file = "pyzmq-25.1.2-cp312-cp312-win32.whl", hash = "sha256:02bbc1a87b76e04fd780b45e7f695471ae6de747769e540da909173d50ff8e2d"}, + {file = "pyzmq-25.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:ced111c2e81506abd1dc142e6cd7b68dd53747b3b7ae5edbea4578c5eeff96b7"}, + {file = "pyzmq-25.1.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:7b6d09a8962a91151f0976008eb7b29b433a560fde056ec7a3db9ec8f1075438"}, + {file = "pyzmq-25.1.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:967668420f36878a3c9ecb5ab33c9d0ff8d054f9c0233d995a6d25b0e95e1b6b"}, + {file = "pyzmq-25.1.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5edac3f57c7ddaacdb4d40f6ef2f9e299471fc38d112f4bc6d60ab9365445fb0"}, + {file = "pyzmq-25.1.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:0dabfb10ef897f3b7e101cacba1437bd3a5032ee667b7ead32bbcdd1a8422fe7"}, + {file = "pyzmq-25.1.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:2c6441e0398c2baacfe5ba30c937d274cfc2dc5b55e82e3749e333aabffde561"}, + {file = "pyzmq-25.1.2-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:16b726c1f6c2e7625706549f9dbe9b06004dfbec30dbed4bf50cbdfc73e5b32a"}, + {file = "pyzmq-25.1.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:a86c2dd76ef71a773e70551a07318b8e52379f58dafa7ae1e0a4be78efd1ff16"}, + {file = "pyzmq-25.1.2-cp36-cp36m-win32.whl", hash = "sha256:359f7f74b5d3c65dae137f33eb2bcfa7ad9ebefd1cab85c935f063f1dbb245cc"}, + {file = "pyzmq-25.1.2-cp36-cp36m-win_amd64.whl", hash = "sha256:55875492f820d0eb3417b51d96fea549cde77893ae3790fd25491c5754ea2f68"}, + {file = "pyzmq-25.1.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b8c8a419dfb02e91b453615c69568442e897aaf77561ee0064d789705ff37a92"}, + {file = "pyzmq-25.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8807c87fa893527ae8a524c15fc505d9950d5e856f03dae5921b5e9aa3b8783b"}, + {file = "pyzmq-25.1.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5e319ed7d6b8f5fad9b76daa0a68497bc6f129858ad956331a5835785761e003"}, + {file = "pyzmq-25.1.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:3c53687dde4d9d473c587ae80cc328e5b102b517447456184b485587ebd18b62"}, + {file = "pyzmq-25.1.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:9add2e5b33d2cd765ad96d5eb734a5e795a0755f7fc49aa04f76d7ddda73fd70"}, + {file = "pyzmq-25.1.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:e690145a8c0c273c28d3b89d6fb32c45e0d9605b2293c10e650265bf5c11cfec"}, + {file = "pyzmq-25.1.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:00a06faa7165634f0cac1abb27e54d7a0b3b44eb9994530b8ec73cf52e15353b"}, + {file = "pyzmq-25.1.2-cp37-cp37m-win32.whl", hash = "sha256:0f97bc2f1f13cb16905a5f3e1fbdf100e712d841482b2237484360f8bc4cb3d7"}, + {file = "pyzmq-25.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6cc0020b74b2e410287e5942e1e10886ff81ac77789eb20bec13f7ae681f0fdd"}, + {file = "pyzmq-25.1.2-cp38-cp38-macosx_10_15_universal2.whl", hash = "sha256:bef02cfcbded83473bdd86dd8d3729cd82b2e569b75844fb4ea08fee3c26ae41"}, + {file = "pyzmq-25.1.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e10a4b5a4b1192d74853cc71a5e9fd022594573926c2a3a4802020360aa719d8"}, + {file = "pyzmq-25.1.2-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8c5f80e578427d4695adac6fdf4370c14a2feafdc8cb35549c219b90652536ae"}, + {file = "pyzmq-25.1.2-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5dde6751e857910c1339890f3524de74007958557593b9e7e8c5f01cd919f8a7"}, + {file = "pyzmq-25.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea1608dd169da230a0ad602d5b1ebd39807ac96cae1845c3ceed39af08a5c6df"}, + {file = "pyzmq-25.1.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0f513130c4c361201da9bc69df25a086487250e16b5571ead521b31ff6b02220"}, + {file = "pyzmq-25.1.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:019744b99da30330798bb37df33549d59d380c78e516e3bab9c9b84f87a9592f"}, + {file = "pyzmq-25.1.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2e2713ef44be5d52dd8b8e2023d706bf66cb22072e97fc71b168e01d25192755"}, + {file = "pyzmq-25.1.2-cp38-cp38-win32.whl", hash = "sha256:07cd61a20a535524906595e09344505a9bd46f1da7a07e504b315d41cd42eb07"}, + {file = "pyzmq-25.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb7e49a17fb8c77d3119d41a4523e432eb0c6932187c37deb6fbb00cc3028088"}, + {file = "pyzmq-25.1.2-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:94504ff66f278ab4b7e03e4cba7e7e400cb73bfa9d3d71f58d8972a8dc67e7a6"}, + {file = "pyzmq-25.1.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6dd0d50bbf9dca1d0bdea219ae6b40f713a3fb477c06ca3714f208fd69e16fd8"}, + {file = "pyzmq-25.1.2-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:004ff469d21e86f0ef0369717351073e0e577428e514c47c8480770d5e24a565"}, + {file = "pyzmq-25.1.2-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c0b5ca88a8928147b7b1e2dfa09f3b6c256bc1135a1338536cbc9ea13d3b7add"}, + {file = "pyzmq-25.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c9a79f1d2495b167119d02be7448bfba57fad2a4207c4f68abc0bab4b92925b"}, + {file = "pyzmq-25.1.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:518efd91c3d8ac9f9b4f7dd0e2b7b8bf1a4fe82a308009016b07eaa48681af82"}, + {file = "pyzmq-25.1.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:1ec23bd7b3a893ae676d0e54ad47d18064e6c5ae1fadc2f195143fb27373f7f6"}, + {file = "pyzmq-25.1.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:db36c27baed588a5a8346b971477b718fdc66cf5b80cbfbd914b4d6d355e44e2"}, + {file = "pyzmq-25.1.2-cp39-cp39-win32.whl", hash = "sha256:39b1067f13aba39d794a24761e385e2eddc26295826530a8c7b6c6c341584289"}, + {file = "pyzmq-25.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:8e9f3fabc445d0ce320ea2c59a75fe3ea591fdbdeebec5db6de530dd4b09412e"}, + {file = "pyzmq-25.1.2-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a8c1d566344aee826b74e472e16edae0a02e2a044f14f7c24e123002dcff1c05"}, + {file = "pyzmq-25.1.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:759cfd391a0996345ba94b6a5110fca9c557ad4166d86a6e81ea526c376a01e8"}, + {file = "pyzmq-25.1.2-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c61e346ac34b74028ede1c6b4bcecf649d69b707b3ff9dc0fab453821b04d1e"}, + {file = "pyzmq-25.1.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4cb8fc1f8d69b411b8ec0b5f1ffbcaf14c1db95b6bccea21d83610987435f1a4"}, + {file = "pyzmq-25.1.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:3c00c9b7d1ca8165c610437ca0c92e7b5607b2f9076f4eb4b095c85d6e680a1d"}, + {file = "pyzmq-25.1.2-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:df0c7a16ebb94452d2909b9a7b3337940e9a87a824c4fc1c7c36bb4404cb0cde"}, + {file = "pyzmq-25.1.2-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:45999e7f7ed5c390f2e87ece7f6c56bf979fb213550229e711e45ecc7d42ccb8"}, + {file = "pyzmq-25.1.2-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ac170e9e048b40c605358667aca3d94e98f604a18c44bdb4c102e67070f3ac9b"}, + {file = "pyzmq-25.1.2-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1b604734bec94f05f81b360a272fc824334267426ae9905ff32dc2be433ab96"}, + {file = "pyzmq-25.1.2-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:a793ac733e3d895d96f865f1806f160696422554e46d30105807fdc9841b9f7d"}, + {file = "pyzmq-25.1.2-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0806175f2ae5ad4b835ecd87f5f85583316b69f17e97786f7443baaf54b9bb98"}, + {file = "pyzmq-25.1.2-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ef12e259e7bc317c7597d4f6ef59b97b913e162d83b421dd0db3d6410f17a244"}, + {file = "pyzmq-25.1.2-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ea253b368eb41116011add00f8d5726762320b1bda892f744c91997b65754d73"}, + {file = "pyzmq-25.1.2-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b9b1f2ad6498445a941d9a4fee096d387fee436e45cc660e72e768d3d8ee611"}, + {file = "pyzmq-25.1.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:8b14c75979ce932c53b79976a395cb2a8cd3aaf14aef75e8c2cb55a330b9b49d"}, + {file = "pyzmq-25.1.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:889370d5174a741a62566c003ee8ddba4b04c3f09a97b8000092b7ca83ec9c49"}, + {file = "pyzmq-25.1.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9a18fff090441a40ffda8a7f4f18f03dc56ae73f148f1832e109f9bffa85df15"}, + {file = "pyzmq-25.1.2-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99a6b36f95c98839ad98f8c553d8507644c880cf1e0a57fe5e3a3f3969040882"}, + {file = "pyzmq-25.1.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4345c9a27f4310afbb9c01750e9461ff33d6fb74cd2456b107525bbeebcb5be3"}, + {file = "pyzmq-25.1.2-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3516e0b6224cf6e43e341d56da15fd33bdc37fa0c06af4f029f7d7dfceceabbc"}, + {file = "pyzmq-25.1.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:146b9b1f29ead41255387fb07be56dc29639262c0f7344f570eecdcd8d683314"}, + {file = "pyzmq-25.1.2.tar.gz", hash = "sha256:93f1aa311e8bb912e34f004cf186407a4e90eec4f0ecc0efd26056bf7eda0226"}, +] + +[package.dependencies] +cffi = {version = "*", markers = "implementation_name == \"pypy\""} + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + +[[package]] +name = "stack-data" +version = "0.6.3" +description = "Extract data from python stack frames and tracebacks for informative displays" +optional = false +python-versions = "*" +files = [ + {file = "stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695"}, + {file = "stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9"}, +] + +[package.dependencies] +asttokens = ">=2.1.0" +executing = ">=1.2.0" +pure-eval = "*" + +[package.extras] +tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"] + +[[package]] +name = "sympy" +version = "1.12" +description = "Computer algebra system (CAS) in Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "sympy-1.12-py3-none-any.whl", hash = "sha256:c3588cd4295d0c0f603d0f2ae780587e64e2efeedb3521e46b9bb1d08d184fa5"}, + {file = "sympy-1.12.tar.gz", hash = "sha256:ebf595c8dac3e0fdc4152c51878b498396ec7f30e7a914d6071e674d49420fb8"}, +] + +[package.dependencies] +mpmath = ">=0.19" + +[[package]] +name = "torch" +version = "2.2.1" +description = "Tensors and Dynamic neural networks in Python with strong GPU acceleration" +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "torch-2.2.1-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:8d3bad336dd2c93c6bcb3268e8e9876185bda50ebde325ef211fb565c7d15273"}, + {file = "torch-2.2.1-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:5297f13370fdaca05959134b26a06a7f232ae254bf2e11a50eddec62525c9006"}, + {file = "torch-2.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:5f5dee8433798888ca1415055f5e3faf28a3bad660e4c29e1014acd3275ab11a"}, + {file = "torch-2.2.1-cp310-none-macosx_10_9_x86_64.whl", hash = "sha256:b6d78338acabf1fb2e88bf4559d837d30230cf9c3e4337261f4d83200df1fcbe"}, + {file = "torch-2.2.1-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:6ab3ea2e29d1aac962e905142bbe50943758f55292f1b4fdfb6f4792aae3323e"}, + {file = "torch-2.2.1-cp311-cp311-manylinux1_x86_64.whl", hash = "sha256:d86664ec85902967d902e78272e97d1aff1d331f7619d398d3ffab1c9b8e9157"}, + {file = "torch-2.2.1-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:d6227060f268894f92c61af0a44c0d8212e19cb98d05c20141c73312d923bc0a"}, + {file = "torch-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:77e990af75fb1675490deb374d36e726f84732cd5677d16f19124934b2409ce9"}, + {file = "torch-2.2.1-cp311-none-macosx_10_9_x86_64.whl", hash = "sha256:46085e328d9b738c261f470231e987930f4cc9472d9ffb7087c7a1343826ac51"}, + {file = "torch-2.2.1-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:2d9e7e5ecbb002257cf98fae13003abbd620196c35f85c9e34c2adfb961321ec"}, + {file = "torch-2.2.1-cp312-cp312-manylinux1_x86_64.whl", hash = "sha256:ada53aebede1c89570e56861b08d12ba4518a1f8b82d467c32665ec4d1f4b3c8"}, + {file = "torch-2.2.1-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:be21d4c41ecebed9e99430dac87de1439a8c7882faf23bba7fea3fea7b906ac1"}, + {file = "torch-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:79848f46196750367dcdf1d2132b722180b9d889571e14d579ae82d2f50596c5"}, + {file = "torch-2.2.1-cp312-none-macosx_10_9_x86_64.whl", hash = "sha256:7ee804847be6be0032fbd2d1e6742fea2814c92bebccb177f0d3b8e92b2d2b18"}, + {file = "torch-2.2.1-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:84b2fb322ab091039fdfe74e17442ff046b258eb5e513a28093152c5b07325a7"}, + {file = "torch-2.2.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:5c0c83aa7d94569997f1f474595e808072d80b04d34912ce6f1a0e1c24b0c12a"}, + {file = "torch-2.2.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:91a1b598055ba06b2c386415d2e7f6ac818545e94c5def597a74754940188513"}, + {file = "torch-2.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:8f93ddf3001ecec16568390b507652644a3a103baa72de3ad3b9c530e3277098"}, + {file = "torch-2.2.1-cp38-none-macosx_10_9_x86_64.whl", hash = "sha256:0e8bdd4c77ac2584f33ee14c6cd3b12767b4da508ec4eed109520be7212d1069"}, + {file = "torch-2.2.1-cp38-none-macosx_11_0_arm64.whl", hash = "sha256:6a21bcd7076677c97ca7db7506d683e4e9db137e8420eb4a68fb67c3668232a7"}, + {file = "torch-2.2.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f1b90ac61f862634039265cd0f746cc9879feee03ff962c803486301b778714b"}, + {file = "torch-2.2.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:ed9e29eb94cd493b36bca9cb0b1fd7f06a0688215ad1e4b3ab4931726e0ec092"}, + {file = "torch-2.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:c47bc25744c743f3835831a20efdcfd60aeb7c3f9804a213f61e45803d16c2a5"}, + {file = "torch-2.2.1-cp39-none-macosx_10_9_x86_64.whl", hash = "sha256:0952549bcb43448c8d860d5e3e947dd18cbab491b14638e21750cb3090d5ad3e"}, + {file = "torch-2.2.1-cp39-none-macosx_11_0_arm64.whl", hash = "sha256:26bd2272ec46fc62dcf7d24b2fb284d44fcb7be9d529ebf336b9860350d674ed"}, +] + +[package.dependencies] +filelock = "*" +fsspec = "*" +jinja2 = "*" +networkx = "*" +nvidia-cublas-cu12 = {version = "12.1.3.1", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} +nvidia-cuda-cupti-cu12 = {version = "12.1.105", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} +nvidia-cuda-nvrtc-cu12 = {version = "12.1.105", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} +nvidia-cuda-runtime-cu12 = {version = "12.1.105", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} +nvidia-cudnn-cu12 = {version = "8.9.2.26", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} +nvidia-cufft-cu12 = {version = "11.0.2.54", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} +nvidia-curand-cu12 = {version = "10.3.2.106", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} +nvidia-cusolver-cu12 = {version = "11.4.5.107", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} +nvidia-cusparse-cu12 = {version = "12.1.0.106", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} +nvidia-nccl-cu12 = {version = "2.19.3", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} +nvidia-nvtx-cu12 = {version = "12.1.105", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} +sympy = "*" +triton = {version = "2.2.0", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version < \"3.12\""} +typing-extensions = ">=4.8.0" + +[package.extras] +opt-einsum = ["opt-einsum (>=3.3)"] +optree = ["optree (>=0.9.1)"] + +[[package]] +name = "tornado" +version = "6.4" +description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." +optional = false +python-versions = ">= 3.8" +files = [ + {file = "tornado-6.4-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:02ccefc7d8211e5a7f9e8bc3f9e5b0ad6262ba2fbb683a6443ecc804e5224ce0"}, + {file = "tornado-6.4-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:27787de946a9cffd63ce5814c33f734c627a87072ec7eed71f7fc4417bb16263"}, + {file = "tornado-6.4-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7894c581ecdcf91666a0912f18ce5e757213999e183ebfc2c3fdbf4d5bd764e"}, + {file = "tornado-6.4-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e43bc2e5370a6a8e413e1e1cd0c91bedc5bd62a74a532371042a18ef19e10579"}, + {file = "tornado-6.4-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0251554cdd50b4b44362f73ad5ba7126fc5b2c2895cc62b14a1c2d7ea32f212"}, + {file = "tornado-6.4-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:fd03192e287fbd0899dd8f81c6fb9cbbc69194d2074b38f384cb6fa72b80e9c2"}, + {file = "tornado-6.4-cp38-abi3-musllinux_1_1_i686.whl", hash = "sha256:88b84956273fbd73420e6d4b8d5ccbe913c65d31351b4c004ae362eba06e1f78"}, + {file = "tornado-6.4-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:71ddfc23a0e03ef2df1c1397d859868d158c8276a0603b96cf86892bff58149f"}, + {file = "tornado-6.4-cp38-abi3-win32.whl", hash = "sha256:6f8a6c77900f5ae93d8b4ae1196472d0ccc2775cc1dfdc9e7727889145c45052"}, + {file = "tornado-6.4-cp38-abi3-win_amd64.whl", hash = "sha256:10aeaa8006333433da48dec9fe417877f8bcc21f48dda8d661ae79da357b2a63"}, + {file = "tornado-6.4.tar.gz", hash = "sha256:72291fa6e6bc84e626589f1c29d90a5a6d593ef5ae68052ee2ef000dfd273dee"}, +] + +[[package]] +name = "traitlets" +version = "5.14.2" +description = "Traitlets Python configuration system" +optional = false +python-versions = ">=3.8" +files = [ + {file = "traitlets-5.14.2-py3-none-any.whl", hash = "sha256:fcdf85684a772ddeba87db2f398ce00b40ff550d1528c03c14dbf6a02003cd80"}, + {file = "traitlets-5.14.2.tar.gz", hash = "sha256:8cdd83c040dab7d1dee822678e5f5d100b514f7b72b01615b26fc5718916fdf9"}, +] + +[package.extras] +docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] +test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0,<8.1)", "pytest-mock", "pytest-mypy-testing"] + +[[package]] +name = "triton" +version = "2.2.0" +description = "A language and compiler for custom Deep Learning operations" +optional = false +python-versions = "*" +files = [ + {file = "triton-2.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2294514340cfe4e8f4f9e5c66c702744c4a117d25e618bd08469d0bfed1e2e5"}, + {file = "triton-2.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da58a152bddb62cafa9a857dd2bc1f886dbf9f9c90a2b5da82157cd2b34392b0"}, + {file = "triton-2.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af58716e721460a61886668b205963dc4d1e4ac20508cc3f623aef0d70283d5"}, + {file = "triton-2.2.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e8fe46d3ab94a8103e291bd44c741cc294b91d1d81c1a2888254cbf7ff846dab"}, + {file = "triton-2.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8ce26093e539d727e7cf6f6f0d932b1ab0574dc02567e684377630d86723ace"}, + {file = "triton-2.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:227cc6f357c5efcb357f3867ac2a8e7ecea2298cd4606a8ba1e931d1d5a947df"}, +] + +[package.dependencies] +filelock = "*" + +[package.extras] +build = ["cmake (>=3.20)", "lit"] +tests = ["autopep8", "flake8", "isort", "numpy", "pytest", "scipy (>=1.7.1)", "torch"] +tutorials = ["matplotlib", "pandas", "tabulate", "torch"] + +[[package]] +name = "typing-extensions" +version = "4.10.0" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475"}, + {file = "typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb"}, +] + +[[package]] +name = "wcwidth" +version = "0.2.13" +description = "Measures the displayed width of unicode strings in a terminal" +optional = false +python-versions = "*" +files = [ + {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, + {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, +] + +[metadata] +lock-version = "2.0" +python-versions = "^3.10" +content-hash = "2c2db0839ccc3d64f21f41acac0451b778ad80406367ed150c9d21ebb6821e13" diff --git a/lasertag/pyproject.toml b/lasertag/pyproject.toml new file mode 100644 index 00000000..d35deba5 --- /dev/null +++ b/lasertag/pyproject.toml @@ -0,0 +1,21 @@ +[tool.poetry] +name = "lasertag" +version = "0.1.0" +description = "" +authors = ["Your Name "] +readme = "README.md" + +[tool.poetry.dependencies] +python = "^3.10" +gym = "^0.26.2" +numpy = "^1.26.4" +torch = "^2.2.1" +griddly = "^1.6.7" + + +[tool.poetry.group.dev.dependencies] +ipykernel = "^6.29.3" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/lasertag/register.py b/lasertag/register.py new file mode 100644 index 00000000..17906b0c --- /dev/null +++ b/lasertag/register.py @@ -0,0 +1,39 @@ +# coding=utf-8 +# Copyright 2021 The Google Research Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Register Lasertag environments with OpenAI gym.""" + +import gym +from gym.envs.registration import register as gym_register + +env_list = [] + + +def register(env_id, entry_point, reward_threshold=0.95, max_episode_steps=None): + """Register a new environment with OpenAI gym based on id.""" + assert env_id.startswith("Lasertag-") + if env_id in env_list: + del gym.envs.registry.env_specs[id] + else: + # Add the environment to the set + env_list.append(id) + + kwargs = dict(id=env_id, entry_point=entry_point, reward_threshold=reward_threshold) + + if max_episode_steps: + kwargs.update({"max_episode_steps": max_episode_steps}) + + # Register the environment with OpenAI gym + gym_register(**kwargs) diff --git a/lasertag_selfplay/fsp_dr.ipynb b/lasertag_selfplay/fsp_dr.ipynb new file mode 100644 index 00000000..cfdd2e30 --- /dev/null +++ b/lasertag_selfplay/fsp_dr.ipynb @@ -0,0 +1,2532 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "import argparse\n", + "import os\n", + "import sys\n", + "import time\n", + "from copy import deepcopy\n", + "from typing import TypeVar, Any, Callable, Dict, Tuple\n", + "from gymnasium.utils.step_api_compatibility import step_api_compatibility\n", + "from multiprocessing import SimpleQueue\n", + "import plotly.express as px\n", + "import numpy as np\n", + "import torch\n", + "import torch.nn as nn\n", + "import joblib\n", + "import torch.optim as optim\n", + "import wandb\n", + "from gymnasium import spaces\n", + "from torch.distributions.categorical import Categorical\n", + "from torch.utils.tensorboard import SummaryWriter\n", + "from tqdm.auto import tqdm\n", + "\n", + "sys.path.append(\"../\")\n", + "\n", + "from lasertag import LasertagAdversarial # noqa: E402\n", + "from syllabus.core import (\n", + " Curriculum,\n", + " TaskWrapper,\n", + " make_multiprocessing_curriculum,\n", + " MultiProcessingSyncWrapper,\n", + " MultiProcessingCurriculumWrapper,\n", + ") # noqa: E402\n", + "from syllabus.curricula import DomainRandomization # noqa: E402\n", + "from syllabus.task_space import TaskSpace # noqa: E402\n", + "\n", + "ObsType = TypeVar(\"ObsType\")\n", + "ActionType = TypeVar(\"ActionType\")\n", + "AgentID = TypeVar(\"AgentID\")\n", + "Agent = TypeVar(\"Agent\")\n", + "EnvTask = TypeVar(\"EnvTask\")\n", + "AgentTask = TypeVar(\"AgentTask\")\n", + "\n", + "\n", + "def batchify(x, device):\n", + " \"\"\"Converts PZ style returns to batch of torch arrays.\"\"\"\n", + " # convert to list of np arrays\n", + " x = np.stack([x[a] for a in x], axis=0)\n", + " # convert to torch\n", + " x = torch.tensor(x).to(device)\n", + "\n", + " return x\n", + "\n", + "\n", + "def unbatchify(x, possible_agents: np.ndarray):\n", + " \"\"\"Converts np array to PZ style arguments.\"\"\"\n", + " x = x.cpu().numpy()\n", + " x = {agent: x[idx] for idx, agent in enumerate(possible_agents)}\n", + "\n", + " return x\n", + "\n", + "\n", + "class LasertagParallelWrapper(TaskWrapper):\n", + " \"\"\"\n", + " Wrapper ensuring compatibility with the PettingZoo Parallel API.\n", + "\n", + " Lasertag Environment:\n", + " * Action shape: `n_agents` * `Discrete(5)`\n", + " * Observation shape: Dict('image': Box(0, 255, (`n_agents`, 3, 5, 5), uint8))\n", + " \"\"\"\n", + "\n", + " def __init__(self, n_agents, *args, **kwargs):\n", + " super().__init__(*args, **kwargs)\n", + " self.n_agents = n_agents\n", + " self.task = None\n", + " self.episode_return = 0\n", + " # self.task_space = TaskSpace(spaces.MultiDiscrete(np.array([[2], [5]])))\n", + " self.possible_agents = [f\"agent_{i}\" for i in range(self.n_agents)]\n", + "\n", + " def __getattr__(self, name):\n", + " \"\"\"\n", + " Delegate attribute lookup to the wrapped environment if the attribute\n", + " is not found in the LasertagParallelWrapper instance.\n", + " \"\"\"\n", + " return getattr(self.env, name)\n", + "\n", + " def _np_array_to_pz_dict(self, array: np.ndarray) -> dict[str : np.ndarray]:\n", + " \"\"\"\n", + " Returns a dictionary containing individual observations for each agent.\n", + " Assumes that the batch dimension represents individual agents.\n", + " \"\"\"\n", + " out = {}\n", + " for idx, value in enumerate(array):\n", + " out[self.possible_agents[idx]] = value\n", + " return out\n", + "\n", + " def _singleton_to_pz_dict(self, value: bool) -> dict[str:bool]:\n", + " \"\"\"\n", + " Broadcasts the `done` and `trunc` flags to dictionaries keyed by agent id.\n", + " \"\"\"\n", + " return {str(agent_index): value for agent_index in range(self.n_agents)}\n", + "\n", + " def reset(\n", + " self, env_task: int\n", + " ) -> tuple[dict[AgentID, ObsType], dict[AgentID, dict]]:\n", + " \"\"\"\n", + " Resets the environment and returns a dictionary of observations\n", + " keyed by agent ID.\n", + " \"\"\"\n", + " self.env.seed(env_task)\n", + " obs = self.env.reset_random() # random level generation\n", + " pz_obs = self._np_array_to_pz_dict(obs[\"image\"])\n", + "\n", + " return pz_obs\n", + "\n", + " def step(\n", + " self, action: dict[AgentID, ActionType], device: str, agent_task: int\n", + " ) -> tuple[\n", + " dict[AgentID, ObsType],\n", + " dict[AgentID, float],\n", + " dict[AgentID, bool],\n", + " dict[AgentID, bool],\n", + " dict[AgentID, dict],\n", + " ]:\n", + " \"\"\"\n", + " Takes inputs in the PettingZoo (PZ) Parallel API format, performs a step and\n", + " returns outputs in PZ format.\n", + " \"\"\"\n", + " action = batchify(action, device)\n", + " obs, rew, done, info = self.env.step(action)\n", + " obs = obs[\"image\"]\n", + " trunc = False # there is no `truncated` flag in this environment\n", + " self.task_completion = self._task_completion(obs, rew, done, trunc, info)\n", + " # convert outputs back to PZ format\n", + " obs, rew = map(self._np_array_to_pz_dict, [obs, rew])\n", + " done, trunc, info = map(\n", + " self._singleton_to_pz_dict, [done, trunc, self.task_completion]\n", + " )\n", + " info[\"agent_id\"] = agent_task\n", + "\n", + " return self.observation(obs), rew, done, trunc, info\n", + "\n", + "\n", + "class FictitiousSelfPlay(Curriculum):\n", + " def __init__(\n", + " self,\n", + " agent,\n", + " storage_path: str,\n", + " max_agents: int,\n", + " ):\n", + " self.name = \"FSP\"\n", + " self.storage_path = storage_path\n", + " if not os.path.exists(self.storage_path):\n", + " # raise FileNotFoundError(f\"Storage path {self.storage_path} not found.\")\n", + " os.makedirs(self.storage_path, exist_ok=True)\n", + "\n", + " self.n_stored_agents = 0\n", + " self.max_agents = max_agents\n", + " self.task_space = TaskSpace(\n", + " spaces.Discrete(self.max_agents)\n", + " )\n", + " self.update_agent(agent) # creates the initial opponent\n", + "\n", + " def update_agent(self, agent):\n", + " \"\"\"Saves the current agent instance to a pickle file.\"\"\"\n", + " if self.n_stored_agents < self.max_agents:\n", + " # TODO: define the expected behaviour when the limit is exceeded\n", + " joblib.dump(\n", + " agent,\n", + " filename=f\"{self.storage_path}/{self.name}_agent_checkpoint_{self.n_stored_agents}.pkl\",\n", + " )\n", + " self.n_stored_agents += 1\n", + "\n", + " def get_opponent(self, agent_id) -> Agent:\n", + " \"\"\"Loads an agent from the buffer of saved agents.\"\"\"\n", + " return joblib.load(\n", + " f\"{self.storage_path}/{self.name}_agent_checkpoint_{agent_id}.pkl\"\n", + " )\n", + "\n", + " def sample(self, k=1):\n", + " return np.random.randint(self.n_stored_agents)\n", + "\n", + "\n", + "class Agent(nn.Module):\n", + " def __init__(self, num_actions):\n", + " super().__init__()\n", + "\n", + " self.network = nn.Sequential(\n", + " self._layer_init(nn.Linear(3 * 5 * 5, 512)),\n", + " nn.ReLU(),\n", + " )\n", + " self.actor = self._layer_init(nn.Linear(512, num_actions), std=0.01)\n", + " self.critic = self._layer_init(nn.Linear(512, 1))\n", + "\n", + " def _layer_init(self, layer, std=np.sqrt(2), bias_const=0.0):\n", + " torch.nn.init.orthogonal_(layer.weight, std)\n", + " torch.nn.init.constant_(layer.bias, bias_const)\n", + " return layer\n", + "\n", + " def get_value(self, x, flatten_start_dim=1):\n", + " x = torch.flatten(x, start_dim=flatten_start_dim)\n", + " return self.critic(self.network(x / 255.0))\n", + "\n", + " def get_action_and_value(self, x, action=None, flatten_start_dim=1):\n", + " x = torch.flatten(x, start_dim=flatten_start_dim)\n", + " hidden = self.network(x / 255.0)\n", + " logits = self.actor(hidden)\n", + " probs = Categorical(logits=logits)\n", + " if action is None:\n", + " action = probs.sample()\n", + " return action, probs.log_prob(action), probs.entropy(), self.critic(hidden)\n", + "\n", + "\n", + "if __name__ == \"__main__\":\n", + "\n", + " \"\"\"ALGO PARAMS\"\"\"\n", + " device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n", + " ent_coef = 0.0\n", + " vf_coef = 0.5\n", + " clip_coef = 0.2\n", + " learning_rate = 1e-4\n", + " epsilon = 1e-5\n", + " gamma = 0.995\n", + " gae_lambda = 0.95\n", + " epochs = 5\n", + " batch_size = 32\n", + " stack_size = 3\n", + " frame_size = (5, 5)\n", + " max_cycles = 201 # lasertag has 200 maximum steps by default\n", + " total_episodes = 500\n", + " n_agents = 2\n", + " num_actions = 5\n", + " fsp_update_frequency = 10\n", + "\n", + " \"\"\" LEARNER SETUP \"\"\"\n", + " agent = Agent(num_actions=num_actions).to(device)\n", + " optimizer = optim.Adam(agent.parameters(), lr=learning_rate, eps=epsilon)\n", + "\n", + " \"\"\" ENV SETUP \"\"\"\n", + " env = LasertagAdversarial(record_video=False) # 2 agents by default\n", + " env = LasertagParallelWrapper(env=env, n_agents=n_agents)\n", + " observation_size = env.observation_space[\"image\"].shape[1:]\n", + "\n", + " \"\"\" ALGO LOGIC: EPISODE STORAGE\"\"\"\n", + " end_step = 0\n", + " total_episodic_return = 0\n", + " rb_obs = torch.zeros((max_cycles, n_agents, stack_size, *frame_size)).to(device)\n", + " rb_actions = torch.zeros((max_cycles, n_agents)).to(device)\n", + " rb_logprobs = torch.zeros((max_cycles, n_agents)).to(device)\n", + " rb_rewards = torch.zeros((max_cycles, n_agents)).to(device)\n", + " rb_terms = torch.zeros((max_cycles, n_agents)).to(device)\n", + " rb_values = torch.zeros((max_cycles, n_agents)).to(device)\n", + "\n", + " losses, episode_rewards = [], []\n", + " agent_c_rew, opp_c_rew = 0, 0\n", + " info = {}" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "class DualCurriculumWrapper:\n", + " \"\"\"Curriculum wrapper containing both an agent and environment-based curriculum.\"\"\"\n", + "\n", + " def __init__(\n", + " self,\n", + " env: TaskWrapper,\n", + " env_curriculum: Curriculum,\n", + " agent_curriculum: Curriculum,\n", + " *args,\n", + " **kwargs,\n", + " ) -> None:\n", + " super().__init__(*args, **kwargs)\n", + " self.env = env\n", + " self.agent_curriculum = agent_curriculum\n", + " self.env_curriculum = env_curriculum\n", + "\n", + " self.env_mp_curriculum, self.env_task_queue, self.env_update_queue = (\n", + " make_multiprocessing_curriculum(env_curriculum)\n", + " )\n", + " self.agent_mp_curriculum, self.agent_task_queue, self.agent_update_queue = (\n", + " make_multiprocessing_curriculum(agent_curriculum)\n", + " )\n", + " self.sample() # initializes env_task and agent_task\n", + "\n", + " def sample(self) -> Tuple[EnvTask, AgentTask]:\n", + " \"\"\"Sets new tasks for the environment and agent curricula.\"\"\"\n", + " self.env_task = self.env_mp_curriculum.sample()\n", + " self.agent_task = self.agent_mp_curriculum.sample()\n", + " return self.env_task, self.agent_task\n", + "\n", + " def get_opponent(self, agent_task: AgentTask) -> Agent:\n", + " return self.agent_mp_curriculum.curriculum.get_opponent(agent_task)\n", + "\n", + " def update_agent(self, agent: Agent) -> Agent:\n", + " return self.agent_mp_curriculum.curriculum.update_agent(agent)\n", + "\n", + " def __getattr__(self, name):\n", + " \"\"\"Delegate attribute lookup to the curricula if not found.\"\"\"\n", + " if hasattr(self.env_curriculum, name):\n", + " return getattr(self.env_curriculum, name)\n", + " elif hasattr(self.agent_curriculum, name):\n", + " return getattr(self.agent_curriculum, name)\n", + " else:\n", + " raise AttributeError(\n", + " f\"'{self.__class__.__name__}' object has no attribute '{name}'\"\n", + " )\n", + "\n", + "\n", + "\n", + "agent_curriculum = FictitiousSelfPlay(\n", + " agent=agent, storage_path=\"fsp_agents\", max_agents=10\n", + ")\n", + "env_curriculum = DomainRandomization(TaskSpace(spaces.Discrete(200)))\n", + "curriculum = DualCurriculumWrapper(\n", + " env=env,\n", + " agent_curriculum=agent_curriculum,\n", + " env_curriculum=env_curriculum,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "abdef1ed70594f698f8c11f97db08597", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + " 0%| | 0/100 [00:00 clip_coef).float().mean().item()\n", + " ]\n", + "\n", + " # normalize advantages\n", + " rb_advantages = b_advantages[batch_index]\n", + " rb_advantages = (rb_advantages - rb_advantages.mean()) / (\n", + " rb_advantages.std() + 1e-8\n", + " )\n", + "\n", + " # Policy loss\n", + " pg_loss1 = -b_advantages[batch_index] * ratio\n", + " pg_loss2 = -b_advantages[batch_index] * torch.clamp(\n", + " ratio, 1 - clip_coef, 1 + clip_coef\n", + " )\n", + " pg_loss = torch.max(pg_loss1, pg_loss2).mean()\n", + "\n", + " # Value loss\n", + " value = value.flatten()\n", + " v_loss_unclipped = (value - b_returns[batch_index]) ** 2\n", + " v_clipped = b_values[batch_index] + torch.clamp(\n", + " value - b_values[batch_index],\n", + " -clip_coef,\n", + " clip_coef,\n", + " )\n", + " v_loss_clipped = (v_clipped - b_returns[batch_index]) ** 2\n", + " v_loss_max = torch.max(v_loss_unclipped, v_loss_clipped)\n", + " v_loss = 0.5 * v_loss_max.mean()\n", + "\n", + " entropy_loss = entropy.mean()\n", + " loss = pg_loss - ent_coef * entropy_loss + v_loss * vf_coef\n", + "\n", + " optimizer.zero_grad()\n", + " loss.backward()\n", + " optimizer.step()\n", + "\n", + " y_pred, y_true = b_values.cpu().numpy(), b_returns.cpu().numpy()\n", + " var_y = np.var(y_true)\n", + " explained_var = np.nan if var_y == 0 else 1 - np.var(y_true - y_pred) / var_y\n", + "\n", + " # update opponent\n", + " if episode % fsp_update_frequency == 0 and episode != 0:\n", + " curriculum.update_agent(agent)\n", + "\n", + " agent_c_rew += rewards[\"agent_0\"]\n", + " opp_c_rew += rewards[\"agent_1\"]\n", + " grid_size = env.level[3][\"grid_size_selected\"]\n", + " walls_percentage = env.level[3][\"clutter_rate_selected\"]" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.plotly.v1+json": { + "config": { + "plotlyServerURL": "https://plotly.com" + }, + "data": [ + { + "alignmentgroup": "True", + "bingroup": "x", + "hovertemplate": "variable=0
value=%{x}
count=%{y}", + "legendgroup": "0", + "marker": { + "color": "#636efa", + "pattern": { + "shape": "" + } + }, + "name": "0", + "offsetgroup": "0", + "orientation": "v", + "showlegend": true, + "type": "histogram", + "x": [ + 3, + 2, + 5, + 2, + 1, + 5, + 2, + 2, + 1, + 3, + 3, + 6, + 3, + 0, + 1, + 3, + 4, + 0, + 6, + 0, + 6, + 6, + 6, + 7, + 1, + 0, + 5, + 1, + 0, + 8, + 2, + 8, + 6, + 2, + 8, + 6, + 5, + 9, + 6, + 9, + 9, + 5, + 0, + 1, + 0, + 9, + 9, + 2, + 1, + 8, + 8, + 7, + 8, + 5, + 2, + 5, + 0, + 4, + 5, + 5, + 4, + 8, + 6, + 9, + 7, + 0, + 9, + 1, + 3, + 1, + 8, + 9, + 0, + 2, + 9, + 0, + 1, + 6, + 9, + 7, + 1, + 8, + 7, + 8, + 7, + 0, + 9, + 0, + 7, + 2, + 1, + 7, + 0, + 0, + 9, + 5, + 6, + 3, + 1, + 8 + ], + "xaxis": "x", + "yaxis": "y" + } + ], + "layout": { + "bargap": 0.2, + "barmode": "relative", + "legend": { + "title": { + "text": "variable" + }, + "tracegroupgap": 0 + }, + "margin": { + "t": 60 + }, + "template": { + "data": { + "bar": [ + { + "error_x": { + "color": "#2a3f5f" + }, + "error_y": { + "color": "#2a3f5f" + }, + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "bar" + } + ], + "barpolar": [ + { + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "barpolar" + } + ], + "carpet": [ + { + "aaxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "baxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "type": "carpet" + } + ], + "choropleth": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "choropleth" + } + ], + "contour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "contour" + } + ], + "contourcarpet": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "contourcarpet" + } + ], + "heatmap": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "heatmap" + } + ], + "heatmapgl": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "heatmapgl" + } + ], + "histogram": [ + { + "marker": { + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "histogram" + } + ], + "histogram2d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "histogram2d" + } + ], + "histogram2dcontour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "histogram2dcontour" + } + ], + "mesh3d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "mesh3d" + } + ], + "parcoords": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "parcoords" + } + ], + "pie": [ + { + "automargin": true, + "type": "pie" + } + ], + "scatter": [ + { + "fillpattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + }, + "type": "scatter" + } + ], + "scatter3d": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatter3d" + } + ], + "scattercarpet": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattercarpet" + } + ], + "scattergeo": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergeo" + } + ], + "scattergl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergl" + } + ], + "scattermapbox": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattermapbox" + } + ], + "scatterpolar": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolar" + } + ], + "scatterpolargl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolargl" + } + ], + "scatterternary": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterternary" + } + ], + "surface": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "surface" + } + ], + "table": [ + { + "cells": { + "fill": { + "color": "#EBF0F8" + }, + "line": { + "color": "white" + } + }, + "header": { + "fill": { + "color": "#C8D4E3" + }, + "line": { + "color": "white" + } + }, + "type": "table" + } + ] + }, + "layout": { + "annotationdefaults": { + "arrowcolor": "#2a3f5f", + "arrowhead": 0, + "arrowwidth": 1 + }, + "autotypenumbers": "strict", + "coloraxis": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "colorscale": { + "diverging": [ + [ + 0, + "#8e0152" + ], + [ + 0.1, + "#c51b7d" + ], + [ + 0.2, + "#de77ae" + ], + [ + 0.3, + "#f1b6da" + ], + [ + 0.4, + "#fde0ef" + ], + [ + 0.5, + "#f7f7f7" + ], + [ + 0.6, + "#e6f5d0" + ], + [ + 0.7, + "#b8e186" + ], + [ + 0.8, + "#7fbc41" + ], + [ + 0.9, + "#4d9221" + ], + [ + 1, + "#276419" + ] + ], + "sequential": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "sequentialminus": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ] + }, + "colorway": [ + "#636efa", + "#EF553B", + "#00cc96", + "#ab63fa", + "#FFA15A", + "#19d3f3", + "#FF6692", + "#B6E880", + "#FF97FF", + "#FECB52" + ], + "font": { + "color": "#2a3f5f" + }, + "geo": { + "bgcolor": "white", + "lakecolor": "white", + "landcolor": "#E5ECF6", + "showlakes": true, + "showland": true, + "subunitcolor": "white" + }, + "hoverlabel": { + "align": "left" + }, + "hovermode": "closest", + "mapbox": { + "style": "light" + }, + "paper_bgcolor": "white", + "plot_bgcolor": "#E5ECF6", + "polar": { + "angularaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "radialaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "scene": { + "xaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "yaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "zaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + } + }, + "shapedefaults": { + "line": { + "color": "#2a3f5f" + } + }, + "ternary": { + "aaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "baxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "caxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "title": { + "x": 0.05 + }, + "xaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + }, + "yaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + } + } + }, + "xaxis": { + "anchor": "y", + "domain": [ + 0, + 1 + ], + "title": { + "text": "value" + } + }, + "yaxis": { + "anchor": "x", + "domain": [ + 0, + 1 + ], + "title": { + "text": "count" + } + } + } + } + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "fig = px.histogram(agent_tasks)\n", + "fig.update_layout(bargap=0.2)\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.plotly.v1+json": { + "config": { + "plotlyServerURL": "https://plotly.com" + }, + "data": [ + { + "alignmentgroup": "True", + "bingroup": "x", + "hovertemplate": "variable=0
value=%{x}
count=%{y}", + "legendgroup": "0", + "marker": { + "color": "#636efa", + "pattern": { + "shape": "" + } + }, + "name": "0", + "offsetgroup": "0", + "orientation": "v", + "showlegend": true, + "type": "histogram", + "x": [ + 21, + 133, + 72, + 128, + 72, + 81, + 103, + 84, + 23, + 140, + 71, + 129, + 17, + 128, + 72, + 171, + 118, + 139, + 179, + 142, + 94, + 69, + 183, + 58, + 152, + 124, + 122, + 84, + 102, + 159, + 130, + 194, + 130, + 164, + 123, + 64, + 148, + 110, + 83, + 186, + 106, + 55, + 147, + 121, + 137, + 122, + 84, + 119, + 140, + 34, + 98, + 42, + 182, + 91, + 178, + 130, + 53, + 44, + 36, + 18, + 197, + 142, + 196, + 31, + 8, + 24, + 107, + 99, + 122, + 70, + 124, + 179, + 142, + 153, + 152, + 124, + 40, + 157, + 158, + 44, + 34, + 98, + 42, + 182, + 160, + 189, + 55, + 147, + 2, + 147, + 121, + 18, + 49, + 114, + 71, + 172, + 173, + 117, + 11, + 153 + ], + "xaxis": "x", + "yaxis": "y" + } + ], + "layout": { + "bargap": 0.2, + "barmode": "relative", + "height": 400, + "legend": { + "title": { + "text": "variable" + }, + "tracegroupgap": 0 + }, + "margin": { + "t": 60 + }, + "template": { + "data": { + "bar": [ + { + "error_x": { + "color": "#2a3f5f" + }, + "error_y": { + "color": "#2a3f5f" + }, + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "bar" + } + ], + "barpolar": [ + { + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "barpolar" + } + ], + "carpet": [ + { + "aaxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "baxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "type": "carpet" + } + ], + "choropleth": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "choropleth" + } + ], + "contour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "contour" + } + ], + "contourcarpet": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "contourcarpet" + } + ], + "heatmap": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "heatmap" + } + ], + "heatmapgl": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "heatmapgl" + } + ], + "histogram": [ + { + "marker": { + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "histogram" + } + ], + "histogram2d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "histogram2d" + } + ], + "histogram2dcontour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "histogram2dcontour" + } + ], + "mesh3d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "mesh3d" + } + ], + "parcoords": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "parcoords" + } + ], + "pie": [ + { + "automargin": true, + "type": "pie" + } + ], + "scatter": [ + { + "fillpattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + }, + "type": "scatter" + } + ], + "scatter3d": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatter3d" + } + ], + "scattercarpet": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattercarpet" + } + ], + "scattergeo": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergeo" + } + ], + "scattergl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergl" + } + ], + "scattermapbox": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattermapbox" + } + ], + "scatterpolar": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolar" + } + ], + "scatterpolargl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolargl" + } + ], + "scatterternary": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterternary" + } + ], + "surface": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "surface" + } + ], + "table": [ + { + "cells": { + "fill": { + "color": "#EBF0F8" + }, + "line": { + "color": "white" + } + }, + "header": { + "fill": { + "color": "#C8D4E3" + }, + "line": { + "color": "white" + } + }, + "type": "table" + } + ] + }, + "layout": { + "annotationdefaults": { + "arrowcolor": "#2a3f5f", + "arrowhead": 0, + "arrowwidth": 1 + }, + "autotypenumbers": "strict", + "coloraxis": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "colorscale": { + "diverging": [ + [ + 0, + "#8e0152" + ], + [ + 0.1, + "#c51b7d" + ], + [ + 0.2, + "#de77ae" + ], + [ + 0.3, + "#f1b6da" + ], + [ + 0.4, + "#fde0ef" + ], + [ + 0.5, + "#f7f7f7" + ], + [ + 0.6, + "#e6f5d0" + ], + [ + 0.7, + "#b8e186" + ], + [ + 0.8, + "#7fbc41" + ], + [ + 0.9, + "#4d9221" + ], + [ + 1, + "#276419" + ] + ], + "sequential": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "sequentialminus": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ] + }, + "colorway": [ + "#636efa", + "#EF553B", + "#00cc96", + "#ab63fa", + "#FFA15A", + "#19d3f3", + "#FF6692", + "#B6E880", + "#FF97FF", + "#FECB52" + ], + "font": { + "color": "#2a3f5f" + }, + "geo": { + "bgcolor": "white", + "lakecolor": "white", + "landcolor": "#E5ECF6", + "showlakes": true, + "showland": true, + "subunitcolor": "white" + }, + "hoverlabel": { + "align": "left" + }, + "hovermode": "closest", + "mapbox": { + "style": "light" + }, + "paper_bgcolor": "white", + "plot_bgcolor": "#E5ECF6", + "polar": { + "angularaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "radialaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "scene": { + "xaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "yaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "zaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + } + }, + "shapedefaults": { + "line": { + "color": "#2a3f5f" + } + }, + "ternary": { + "aaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "baxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "caxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "title": { + "x": 0.05 + }, + "xaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + }, + "yaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + } + } + }, + "xaxis": { + "anchor": "y", + "domain": [ + 0, + 1 + ], + "title": { + "text": "value" + } + }, + "yaxis": { + "anchor": "x", + "domain": [ + 0, + 1 + ], + "title": { + "text": "count" + } + } + } + } + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "fig = px.histogram(env_tasks, height=400)\n", + "fig.update_layout(bargap=0.2)\n", + "fig.show()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.7" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/lasertag_selfplay/lasertag_fsp_dr_adversarial.py b/lasertag_selfplay/lasertag_fsp_dr_adversarial.py new file mode 100644 index 00000000..c6c1ef68 --- /dev/null +++ b/lasertag_selfplay/lasertag_fsp_dr_adversarial.py @@ -0,0 +1,546 @@ +import argparse +import os +import sys +import time +from typing import Tuple, TypeVar + +import joblib +import numpy as np +import plotly +import plotly.express as px +import torch +import torch.nn as nn +import torch.optim as optim +import wandb +from gymnasium import spaces +from torch.distributions.categorical import Categorical +from torch.utils.tensorboard import SummaryWriter +from tqdm.auto import tqdm + +sys.path.append("../") + +from lasertag import LasertagAdversarial # noqa: E402 +from syllabus.core import ( # noqa: E402 + Curriculum, + TaskWrapper, + make_multiprocessing_curriculum, +) +from syllabus.curricula import DomainRandomization # noqa: E402 +from syllabus.task_space import TaskSpace # noqa: E402 + +ObsType = TypeVar("ObsType") +ActionType = TypeVar("ActionType") +AgentID = TypeVar("AgentID") +Agent = TypeVar("Agent") +EnvTask = TypeVar("EnvTask") +AgentTask = TypeVar("AgentTask") + + +def parse_args(): + parser = argparse.ArgumentParser() + parser.add_argument("--track", type=bool, default=False) + parser.add_argument( + "--exp-name", + type=str, + default=os.path.basename(__file__).rstrip(".py"), + help="the name of this experiment", + ) + parser.add_argument( + "--wandb-project-name", + type=str, + default="syllabus", + help="the wandb's project name", + ) + parser.add_argument( + "--wandb-entity", + type=str, + default="rpegoud", + help="the entity (team) of wandb's project", + ) + parser.add_argument( + "--logging-dir", + type=str, + default=".", + help="the base directory for logging and wandb storage.", + ) + + args = parser.parse_args() + return args + + +def batchify(x, device): + """Converts PZ style returns to batch of torch arrays.""" + # convert to list of np arrays + x = np.stack([x[a] for a in x], axis=0) + # convert to torch + x = torch.tensor(x).to(device) + + return x + + +def unbatchify(x, possible_agents: np.ndarray): + """Converts np array to PZ style arguments.""" + x = x.cpu().numpy() + x = {agent: x[idx] for idx, agent in enumerate(possible_agents)} + + return x + + +class LasertagParallelWrapper(TaskWrapper): + """ + Wrapper ensuring compatibility with the PettingZoo Parallel API. + + Lasertag Environment: + * Action shape: `n_agents` * `Discrete(5)` + * Observation shape: Dict('image': Box(0, 255, (`n_agents`, 3, 5, 5), uint8)) + """ + + def __init__(self, n_agents, *args, **kwargs): + super().__init__(*args, **kwargs) + self.n_agents = n_agents + self.task = None + self.episode_return = 0 + # self.task_space = TaskSpace(spaces.MultiDiscrete(np.array([[2], [5]]))) + self.possible_agents = [f"agent_{i}" for i in range(self.n_agents)] + + def __getattr__(self, name): + """ + Delegate attribute lookup to the wrapped environment if the attribute + is not found in the LasertagParallelWrapper instance. + """ + return getattr(self.env, name) + + def _np_array_to_pz_dict(self, array: np.ndarray) -> dict[str : np.ndarray]: + """ + Returns a dictionary containing individual observations for each agent. + Assumes that the batch dimension represents individual agents. + """ + out = {} + for idx, value in enumerate(array): + out[self.possible_agents[idx]] = value + return out + + def _singleton_to_pz_dict(self, value: bool) -> dict[str:bool]: + """ + Broadcasts the `done` and `trunc` flags to dictionaries keyed by agent id. + """ + return {str(agent_index): value for agent_index in range(self.n_agents)} + + def reset( + self, env_task: int + ) -> tuple[dict[AgentID, ObsType], dict[AgentID, dict]]: + """ + Resets the environment and returns a dictionary of observations + keyed by agent ID. + """ + self.env.seed(env_task) + obs = self.env.reset_random() # random level generation + pz_obs = self._np_array_to_pz_dict(obs["image"]) + + return pz_obs + + def step( + self, action: dict[AgentID, ActionType], device: str, agent_task: int + ) -> tuple[ + dict[AgentID, ObsType], + dict[AgentID, float], + dict[AgentID, bool], + dict[AgentID, bool], + dict[AgentID, dict], + ]: + """ + Takes inputs in the PettingZoo (PZ) Parallel API format, performs a step and + returns outputs in PZ format. + """ + action = batchify(action, device) + obs, rew, done, info = self.env.step(action) + obs = obs["image"] + trunc = False # there is no `truncated` flag in this environment + self.task_completion = self._task_completion(obs, rew, done, trunc, info) + # convert outputs back to PZ format + obs, rew = map(self._np_array_to_pz_dict, [obs, rew]) + done, trunc, info = map( + self._singleton_to_pz_dict, [done, trunc, self.task_completion] + ) + info["agent_id"] = agent_task + + return self.observation(obs), rew, done, trunc, info + + +class FictitiousSelfPlay(Curriculum): + def __init__( + self, + agent, + storage_path: str, + max_agents: int, + ): + self.name = "FSP" + self.storage_path = storage_path + if not os.path.exists(self.storage_path): + # raise FileNotFoundError(f"Storage path {self.storage_path} not found.") + os.makedirs(self.storage_path, exist_ok=True) + + self.n_stored_agents = 0 + self.max_agents = max_agents + self.task_space = TaskSpace(spaces.Discrete(self.max_agents)) + self.update_agent(agent) # creates the initial opponent + + def update_agent(self, agent): + """Saves the current agent instance to a pickle file.""" + if self.n_stored_agents < self.max_agents: + # TODO: define the expected behaviour when the limit is exceeded + joblib.dump( + agent, + filename=f"{self.storage_path}/{self.name}_agent_checkpoint_{self.n_stored_agents}.pkl", + ) + self.n_stored_agents += 1 + + def get_opponent(self, agent_id) -> Agent: + """Loads an agent from the buffer of saved agents.""" + return joblib.load( + f"{self.storage_path}/{self.name}_agent_checkpoint_{agent_id}.pkl" + ) + + def sample(self, k=1): + return np.random.randint(self.n_stored_agents) + + +class Agent(nn.Module): + def __init__(self, num_actions): + super().__init__() + + self.network = nn.Sequential( + self._layer_init(nn.Linear(3 * 5 * 5, 512)), + nn.ReLU(), + ) + self.actor = self._layer_init(nn.Linear(512, num_actions), std=0.01) + self.critic = self._layer_init(nn.Linear(512, 1)) + + def _layer_init(self, layer, std=np.sqrt(2), bias_const=0.0): + torch.nn.init.orthogonal_(layer.weight, std) + torch.nn.init.constant_(layer.bias, bias_const) + return layer + + def get_value(self, x, flatten_start_dim=1): + x = torch.flatten(x, start_dim=flatten_start_dim) + return self.critic(self.network(x / 255.0)) + + def get_action_and_value(self, x, action=None, flatten_start_dim=1): + x = torch.flatten(x, start_dim=flatten_start_dim) + hidden = self.network(x / 255.0) + logits = self.actor(hidden) + probs = Categorical(logits=logits) + if action is None: + action = probs.sample() + return action, probs.log_prob(action), probs.entropy(), self.critic(hidden) + + +class DualCurriculumWrapper: + """Curriculum wrapper containing both an agent and environment-based curriculum.""" + + def __init__( + self, + env: TaskWrapper, + env_curriculum: Curriculum, + agent_curriculum: Curriculum, + *args, + **kwargs, + ) -> None: + super().__init__(*args, **kwargs) + self.env = env + self.agent_curriculum = agent_curriculum + self.env_curriculum = env_curriculum + + self.env_mp_curriculum, self.env_task_queue, self.env_update_queue = ( + make_multiprocessing_curriculum(env_curriculum) + ) + self.agent_mp_curriculum, self.agent_task_queue, self.agent_update_queue = ( + make_multiprocessing_curriculum(agent_curriculum) + ) + self.sample() # initializes env_task and agent_task + + def sample(self) -> Tuple[EnvTask, AgentTask]: + """Sets new tasks for the environment and agent curricula.""" + self.env_task = self.env_mp_curriculum.sample() + self.agent_task = self.agent_mp_curriculum.sample() + return self.env_task, self.agent_task + + def get_opponent(self, agent_task: AgentTask) -> Tuple[Agent, AgentTask]: + return self.agent_mp_curriculum.curriculum.get_opponent(agent_task) + + def update_agent(self, agent: Agent) -> Agent: + return self.agent_mp_curriculum.curriculum.update_agent(agent) + + def __getattr__(self, name): + """Delegate attribute lookup to the curricula if not found.""" + if hasattr(self.env_curriculum, name): + return getattr(self.env_curriculum, name) + elif hasattr(self.agent_curriculum, name): + return getattr(self.agent_curriculum, name) + else: + raise AttributeError( + f"'{self.__class__.__name__}' object has no attribute '{name}'" + ) + + +if __name__ == "__main__": + args = parse_args() + run_name = f"lasertag__{args.exp_name}__{int(time.time())}" + if args.track: + wandb.init( + project=args.wandb_project_name, + entity=args.wandb_entity, + sync_tensorboard=True, + config=vars(args), + name=run_name, + monitor_gym=True, + save_code=True, + dir=args.logging_dir, + ) + wandb.run.log_code(os.path.join(args.logging_dir, "/syllabus/examples")) + + writer = SummaryWriter(os.path.join(args.logging_dir, "./runs/{run_name}")) + writer.add_text( + "hyperparameters", + "|param|value|\n|-|-|\n%s" + % ("\n".join([f"|{key}|{value}|" for key, value in vars(args).items()])), + ) + + """ALGO PARAMS""" + device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + ent_coef = 0.0 + vf_coef = 0.5 + clip_coef = 0.2 + learning_rate = 1e-4 + epsilon = 1e-5 + gamma = 0.995 + gae_lambda = 0.95 + epochs = 5 + batch_size = 32 + stack_size = 3 + frame_size = (5, 5) + max_cycles = 201 # lasertag has 200 maximum steps by default + total_episodes = 5000 + n_agents = 2 + num_actions = 5 + max_agents = 10 + fsp_update_frequency = total_episodes / max_agents + + """ LEARNER SETUP """ + agent = Agent(num_actions=num_actions).to(device) + optimizer = optim.Adam(agent.parameters(), lr=learning_rate, eps=epsilon) + + """ ENV SETUP """ + env = LasertagAdversarial(record_video=False) # 2 agents by default + env = LasertagParallelWrapper(env=env, n_agents=n_agents) + agent_curriculum = FictitiousSelfPlay( + agent=agent, storage_path="fsp_agents", max_agents=max_agents + ) + observation_size = env.observation_space["image"].shape[1:] + env_curriculum = DomainRandomization(TaskSpace(spaces.Discrete(200))) + curriculum = DualCurriculumWrapper( + env=env, + agent_curriculum=agent_curriculum, + env_curriculum=env_curriculum, + ) + + """ ALGO LOGIC: EPISODE STORAGE""" + end_step = 0 + total_episodic_return = 0 + rb_obs = torch.zeros((max_cycles, n_agents, stack_size, *frame_size)).to(device) + rb_actions = torch.zeros((max_cycles, n_agents)).to(device) + rb_logprobs = torch.zeros((max_cycles, n_agents)).to(device) + rb_rewards = torch.zeros((max_cycles, n_agents)).to(device) + rb_terms = torch.zeros((max_cycles, n_agents)).to(device) + rb_values = torch.zeros((max_cycles, n_agents)).to(device) + + agent_tasks, env_tasks = [], [] + agent_c_rew, opp_c_rew = 0, 0 + n_ends, n_learner_wins = 0, 0 + info = {} + + """ TRAINING LOGIC """ + # train for n number of episodes + for episode in tqdm(range(total_episodes)): + # collect an episode + with torch.no_grad(): + # collect observations and convert to batch of torch tensors + env_task, agent_task = curriculum.sample() + + env_tasks.append(env_task[0]) + agent_tasks.append(agent_task) + + next_obs = env.reset(env_task) + # reset the episodic return + total_episodic_return = 0 + n_steps = 0 + + # each episode has num_steps + for step in range(0, max_cycles): + # rollover the observation + joint_obs = batchify(next_obs, device).squeeze() + agent_obs, opponent_obs = joint_obs + + # get action from the agent and the opponent + actions, logprobs, _, values = agent.get_action_and_value( + agent_obs, flatten_start_dim=0 + ) + + opponent = curriculum.get_opponent(info.get("agent_id", 0)).to(device) + opponent_action, *_ = opponent.get_action_and_value( + opponent_obs, flatten_start_dim=0 + ) + # execute the environment and log data + joint_actions = torch.tensor((actions, opponent_action)) + next_obs, rewards, terms, truncs, info = env.step( + unbatchify(joint_actions, env.possible_agents), device, agent_task + ) + + opp_reward = rewards["agent_1"] + if opp_reward != 0: + n_ends += 1 + if opp_reward == -1: + n_learner_wins += 1 + + # add to episode storage + rb_obs[step] = batchify(next_obs, device) + rb_rewards[step] = batchify(rewards, device) + rb_terms[step] = batchify(terms, device) + rb_actions[step] = joint_actions + rb_logprobs[step] = logprobs + rb_values[step] = values.flatten() + + # compute episodic return + total_episodic_return += rb_rewards[step].cpu().numpy() + + # if we reach termination or truncation, end + if any([terms[a] for a in terms]) or any([truncs[a] for a in truncs]): + end_step = step + break + + with torch.no_grad(): + next_value = agent.get_value( + torch.tensor(next_obs["agent_0"]), flatten_start_dim=0 + ) + rb_advantages = torch.zeros_like(rb_rewards).to(device) + last_gae_lam = 0 + for t in reversed(range(end_step)): + if t == end_step - 1: + next_non_terminal = 1.0 - rb_terms[t + 1] + next_values = next_value + else: + next_non_terminal = 1.0 - rb_terms[t + 1] + next_values = rb_values[t + 1] + delta = ( + rb_rewards[t] + + gamma * next_values * next_non_terminal + - rb_values[t] + ) + rb_advantages[t] = last_gae_lam = ( + delta + gamma * gae_lambda * next_non_terminal * last_gae_lam + ) + rb_returns = rb_advantages + rb_values + # convert our episodes to batch of individual transitions + b_obs = torch.flatten(rb_obs[:end_step], start_dim=0, end_dim=1) + b_logprobs = torch.flatten(rb_logprobs[:end_step], start_dim=0, end_dim=1) + b_actions = torch.flatten(rb_actions[:end_step], start_dim=0, end_dim=1) + b_returns = torch.flatten(rb_returns[:end_step], start_dim=0, end_dim=1) + b_values = torch.flatten(rb_values[:end_step], start_dim=0, end_dim=1) + b_advantages = torch.flatten(rb_advantages[:end_step], start_dim=0, end_dim=1) + + # Optimizing the policy and value network + b_index = np.arange(len(b_obs)) + clip_fracs = [] + for repeat in range(epochs): + # shuffle the indices we use to access the data + np.random.shuffle(b_index) + for start in range(0, len(b_obs), batch_size): + # select the indices we want to train on + end = start + batch_size + batch_index = b_index[start:end] + + _, newlogprob, entropy, value = agent.get_action_and_value( + b_obs[batch_index], b_actions.long()[batch_index] + ) + logratio = newlogprob - b_logprobs[batch_index] + ratio = logratio.exp() + + with torch.no_grad(): + # calculate approx_kl http://joschu.net/blog/kl-approx.html + old_approx_kl = (-logratio).mean() + approx_kl = ((ratio - 1) - logratio).mean() + clip_fracs += [ + ((ratio - 1.0).abs() > clip_coef).float().mean().item() + ] + + # normalize advantages + rb_advantages = b_advantages[batch_index] + rb_advantages = (rb_advantages - rb_advantages.mean()) / ( + rb_advantages.std() + 1e-8 + ) + + # Policy loss + pg_loss1 = -b_advantages[batch_index] * ratio + pg_loss2 = -b_advantages[batch_index] * torch.clamp( + ratio, 1 - clip_coef, 1 + clip_coef + ) + pg_loss = torch.max(pg_loss1, pg_loss2).mean() + + # Value loss + value = value.flatten() + v_loss_unclipped = (value - b_returns[batch_index]) ** 2 + v_clipped = b_values[batch_index] + torch.clamp( + value - b_values[batch_index], + -clip_coef, + clip_coef, + ) + v_loss_clipped = (v_clipped - b_returns[batch_index]) ** 2 + v_loss_max = torch.max(v_loss_unclipped, v_loss_clipped) + v_loss = 0.5 * v_loss_max.mean() + + entropy_loss = entropy.mean() + loss = pg_loss - ent_coef * entropy_loss + v_loss * vf_coef + + optimizer.zero_grad() + loss.backward() + optimizer.step() + + y_pred, y_true = b_values.cpu().numpy(), b_returns.cpu().numpy() + var_y = np.var(y_true) + explained_var = np.nan if var_y == 0 else 1 - np.var(y_true - y_pred) / var_y + + # update opponent + if episode % fsp_update_frequency == 0 and episode != 0: + curriculum.update_agent(agent) + + agent_c_rew += rewards["agent_0"] + opp_c_rew += rewards["agent_1"] + grid_size = env.level[3]["grid_size_selected"] + walls_percentage = env.level[3]["clutter_rate_selected"] + + writer.add_scalar("charts/steps_per_ep", end_step, episode) + writer.add_scalar("charts/agent_reward", agent_c_rew, episode) + writer.add_scalar("charts/opponent_reward", opp_c_rew, episode) + writer.add_scalar("charts/grid_size", grid_size, episode) + writer.add_scalar("charts/walls_percentage", walls_percentage, episode) + writer.add_scalar("losses/value_loss", v_loss.item(), episode) + writer.add_scalar("losses/policy_loss", pg_loss.item(), episode) + writer.add_scalar("losses/entropy", entropy_loss.item(), episode) + writer.add_scalar("losses/old_approx_kl", old_approx_kl.item(), episode) + writer.add_scalar("losses/approx_kl", approx_kl.item(), episode) + + learner_winrate = n_learner_wins / n_ends + wandb.run.summary["n_episodes"] = total_episodes + wandb.run.summary["learner_winrate"] = learner_winrate + writer.add_scalar("charts/learner_winrate", learner_winrate) + + fig = px.histogram(agent_tasks, height=400) + fig.update_layout(bargap=0.2, showlegend=False) + wandb.log({"charts/agent_tasks": wandb.Html(plotly.io.to_html(fig))}) + + fig = px.histogram(env_tasks, height=400) + fig.update_layout(bargap=0.2, showlegend=False) + + wandb.log({"charts/env_tasks": wandb.Html(plotly.io.to_html(fig))}) + + writer.close() diff --git a/lasertag_selfplay/lasertag_pfsp_dr_adversarial.py b/lasertag_selfplay/lasertag_pfsp_dr_adversarial.py new file mode 100644 index 00000000..2f988b6a --- /dev/null +++ b/lasertag_selfplay/lasertag_pfsp_dr_adversarial.py @@ -0,0 +1,608 @@ +import argparse +import os +import sys +import time +from typing import Tuple, TypeVar + +import joblib +import numpy as np +import plotly +import plotly.express as px +import plotly.graph_objects as go +import torch +import torch.nn as nn +import torch.optim as optim +import wandb +from gymnasium import spaces +from plotly.subplots import make_subplots +from scipy.special import softmax +from torch.distributions.categorical import Categorical +from torch.utils.tensorboard import SummaryWriter +from tqdm.auto import tqdm + +sys.path.append("../") + +from lasertag import LasertagAdversarial # noqa: E402 +from syllabus.core import ( # noqa: E402 + Curriculum, + TaskWrapper, + make_multiprocessing_curriculum, +) +from syllabus.curricula import DomainRandomization # noqa: E402 +from syllabus.task_space import TaskSpace # noqa: E402 + +ObsType = TypeVar("ObsType") +ActionType = TypeVar("ActionType") +AgentID = TypeVar("AgentID") +Agent = TypeVar("Agent") +EnvTask = TypeVar("EnvTask") +AgentTask = TypeVar("AgentTask") + + +def parse_args(): + parser = argparse.ArgumentParser() + parser.add_argument("--track", type=bool, default=False) + parser.add_argument( + "--exp-name", + type=str, + default=os.path.basename(__file__).rstrip(".py"), + help="the name of this experiment", + ) + parser.add_argument( + "--wandb-project-name", + type=str, + default="syllabus", + help="the wandb's project name", + ) + parser.add_argument( + "--wandb-entity", + type=str, + default="rpegoud", + help="the entity (team) of wandb's project", + ) + parser.add_argument( + "--logging-dir", + type=str, + default=".", + help="the base directory for logging and wandb storage.", + ) + + args = parser.parse_args() + return args + + +def batchify(x, device): + """Converts PZ style returns to batch of torch arrays.""" + # convert to list of np arrays + x = np.stack([x[a] for a in x], axis=0) + # convert to torch + x = torch.tensor(x).to(device) + + return x + + +def unbatchify(x, possible_agents: np.ndarray): + """Converts np array to PZ style arguments.""" + x = x.cpu().numpy() + x = {agent: x[idx] for idx, agent in enumerate(possible_agents)} + + return x + + +class LasertagParallelWrapper(TaskWrapper): + """ + Wrapper ensuring compatibility with the PettingZoo Parallel API. + + Lasertag Environment: + * Action shape: `n_agents` * `Discrete(5)` + * Observation shape: Dict('image': Box(0, 255, (`n_agents`, 3, 5, 5), uint8)) + """ + + def __init__(self, n_agents, *args, **kwargs): + super().__init__(*args, **kwargs) + self.n_agents = n_agents + self.task = None + self.episode_return = 0 + # self.task_space = TaskSpace(spaces.MultiDiscrete(np.array([[2], [5]]))) + self.possible_agents = [f"agent_{i}" for i in range(self.n_agents)] + + def __getattr__(self, name): + """ + Delegate attribute lookup to the wrapped environment if the attribute + is not found in the LasertagParallelWrapper instance. + """ + return getattr(self.env, name) + + def _np_array_to_pz_dict(self, array: np.ndarray) -> dict[str : np.ndarray]: + """ + Returns a dictionary containing individual observations for each agent. + Assumes that the batch dimension represents individual agents. + """ + out = {} + for idx, value in enumerate(array): + out[self.possible_agents[idx]] = value + return out + + def _singleton_to_pz_dict(self, value: bool) -> dict[str:bool]: + """ + Broadcasts the `done` and `trunc` flags to dictionaries keyed by agent id. + """ + return {str(agent_index): value for agent_index in range(self.n_agents)} + + def reset( + self, env_task: int + ) -> tuple[dict[AgentID, ObsType], dict[AgentID, dict]]: + """ + Resets the environment and returns a dictionary of observations + keyed by agent ID. + """ + self.env.seed(env_task) + obs = self.env.reset_random() # random level generation + pz_obs = self._np_array_to_pz_dict(obs["image"]) + + return pz_obs + + def step( + self, action: dict[AgentID, ActionType], device: str, agent_task: int + ) -> tuple[ + dict[AgentID, ObsType], + dict[AgentID, float], + dict[AgentID, bool], + dict[AgentID, bool], + dict[AgentID, dict], + ]: + """ + Takes inputs in the PettingZoo (PZ) Parallel API format, performs a step and + returns outputs in PZ format. + """ + action = batchify(action, device) + obs, rew, done, info = self.env.step(action) + obs = obs["image"] + trunc = False # there is no `truncated` flag in this environment + self.task_completion = self._task_completion(obs, rew, done, trunc, info) + # convert outputs back to PZ format + obs, rew = map(self._np_array_to_pz_dict, [obs, rew]) + done, trunc, info = map( + self._singleton_to_pz_dict, [done, trunc, self.task_completion] + ) + info["agent_id"] = agent_task + + return self.observation(obs), rew, done, trunc, info + + +class PrioritizedFictitiousSelfPlay(Curriculum): + def __init__( + self, + agent, + storage_path: str, + max_agents: int, + ): + self.name = "PFSP" + self.storage_path = storage_path + if not os.path.exists(self.storage_path): + os.makedirs(self.storage_path, exist_ok=True) + + self.n_stored_agents = 0 + self.max_agents = max_agents + self.task_space = TaskSpace(spaces.Discrete(self.max_agents)) + self.update_agent(agent) # creates the initial opponent + self.history = { + i: { + "winrate": 0, + "n_games": 0, + } + for i in range(self.max_agents) + } + + def update_agent(self, agent) -> None: + """ + Saves the current agent instance to a pickle file and update + its priority. + """ + if self.n_stored_agents < self.max_agents: + # TODO: define the expected behaviour when the limit is exceeded + joblib.dump( + agent, + filename=f"{self.storage_path}/{self.name}_agent_checkpoint_{self.n_stored_agents}.pkl", + ) + self.n_stored_agents += 1 + + def update_winrate(self, opponent_id: int, opponent_reward: int) -> None: + """ + Uses an incremental mean to update the opponent's winrate i.e. priority. + This implies that sampling according to the winrates returns the most + challenging opponents. + """ + opponent_reward = opponent_reward > 0 # converts the reward to 0 or 1 + self.history[opponent_id]["n_games"] += 1 + old_winrate = self.history[opponent_id]["winrate"] + n = self.history[opponent_id]["n_games"] + + self.history[opponent_id]["winrate"] = ( + old_winrate + (opponent_reward - old_winrate) / n + ) + + def get_opponent(self, agent_id: int) -> Agent: + """ + Samples an agent id from the softmax distribution induced by winrates + then loads the selected agent from the buffer of saved agents. + """ + return joblib.load( + f"{self.storage_path}/{self.name}_agent_checkpoint_{agent_id}.pkl" + ).to(device) + + def sample(self, k=1): + logits = [ + self.history[agent_id]["winrate"] + for agent_id in range(self.n_stored_agents) + ] + return np.random.choice( + np.arange(self.n_stored_agents), + p=softmax(logits), + ) + + +class Agent(nn.Module): + def __init__(self, num_actions): + super().__init__() + + self.network = nn.Sequential( + self._layer_init(nn.Linear(3 * 5 * 5, 512)), + nn.ReLU(), + ) + self.actor = self._layer_init(nn.Linear(512, num_actions), std=0.01) + self.critic = self._layer_init(nn.Linear(512, 1)) + + def _layer_init(self, layer, std=np.sqrt(2), bias_const=0.0): + torch.nn.init.orthogonal_(layer.weight, std) + torch.nn.init.constant_(layer.bias, bias_const) + return layer + + def get_value(self, x, flatten_start_dim=1): + x = torch.flatten(x, start_dim=flatten_start_dim) + return self.critic(self.network(x / 255.0)) + + def get_action_and_value(self, x, action=None, flatten_start_dim=1): + x = torch.flatten(x, start_dim=flatten_start_dim) + hidden = self.network(x / 255.0) + logits = self.actor(hidden) + probs = Categorical(logits=logits) + if action is None: + action = probs.sample() + return action, probs.log_prob(action), probs.entropy(), self.critic(hidden) + + +class DualCurriculumWrapper: + """Curriculum wrapper containing both an agent and environment-based curriculum.""" + + def __init__( + self, + env: TaskWrapper, + env_curriculum: Curriculum, + agent_curriculum: Curriculum, + *args, + **kwargs, + ) -> None: + super().__init__(*args, **kwargs) + self.env = env + self.agent_curriculum = agent_curriculum + self.env_curriculum = env_curriculum + + self.env_mp_curriculum, self.env_task_queue, self.env_update_queue = ( + make_multiprocessing_curriculum(env_curriculum) + ) + self.agent_mp_curriculum, self.agent_task_queue, self.agent_update_queue = ( + make_multiprocessing_curriculum(agent_curriculum) + ) + self.sample() # initializes env_task and agent_task + + def sample(self) -> Tuple[EnvTask, AgentTask]: + """Sets new tasks for the environment and agent curricula.""" + self.env_task = self.env_mp_curriculum.sample() + self.agent_task = self.agent_mp_curriculum.sample() + return self.env_task, self.agent_task + + def get_opponent(self, agent_task: AgentTask) -> Agent: + return self.agent_mp_curriculum.curriculum.get_opponent(agent_task) + + def update_agent(self, agent: Agent) -> Agent: + return self.agent_mp_curriculum.curriculum.update_agent(agent) + + def __getattr__(self, name): + """Delegate attribute lookup to the curricula if not found.""" + if hasattr(self.env_curriculum, name): + return getattr(self.env_curriculum, name) + elif hasattr(self.agent_curriculum, name): + return getattr(self.agent_curriculum, name) + else: + raise AttributeError( + f"'{self.__class__.__name__}' object has no attribute '{name}'" + ) + + +if __name__ == "__main__": + args = parse_args() + run_name = f"lasertag__{args.exp_name}__{int(time.time())}" + if args.track: + wandb.init( + project=args.wandb_project_name, + entity=args.wandb_entity, + sync_tensorboard=True, + config=vars(args), + name=run_name, + monitor_gym=True, + save_code=True, + dir=args.logging_dir, + ) + wandb.run.log_code(os.path.join(args.logging_dir, "/syllabus/examples")) + + writer = SummaryWriter(os.path.join(args.logging_dir, "./runs/{run_name}")) + writer.add_text( + "hyperparameters", + "|param|value|\n|-|-|\n%s" + % ("\n".join([f"|{key}|{value}|" for key, value in vars(args).items()])), + ) + + """ALGO PARAMS""" + device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + ent_coef = 0.0 + vf_coef = 0.5 + clip_coef = 0.2 + learning_rate = 1e-4 + epsilon = 1e-5 + gamma = 0.995 + gae_lambda = 0.95 + epochs = 5 + batch_size = 32 + stack_size = 3 + frame_size = (5, 5) + max_cycles = 201 # lasertag has 200 maximum steps by default + total_episodes = 500 + n_agents = 2 + num_actions = 5 + fsp_update_frequency = 50 + + """ LEARNER SETUP """ + agent = Agent(num_actions=num_actions).to(device) + optimizer = optim.Adam(agent.parameters(), lr=learning_rate, eps=epsilon) + + """ ENV SETUP """ + env = LasertagAdversarial(record_video=False) # 2 agents by default + env = LasertagParallelWrapper(env=env, n_agents=n_agents) + agent_curriculum = PrioritizedFictitiousSelfPlay( + agent=agent, storage_path="pfsp_agents", max_agents=10 + ) + observation_size = env.observation_space["image"].shape[1:] + env_curriculum = DomainRandomization(TaskSpace(spaces.Discrete(200))) + curriculum = DualCurriculumWrapper( + env=env, + agent_curriculum=agent_curriculum, + env_curriculum=env_curriculum, + ) + + """ ALGO LOGIC: EPISODE STORAGE""" + end_step = 0 + total_episodic_return = 0 + rb_obs = torch.zeros((max_cycles, n_agents, stack_size, *frame_size)).to(device) + rb_actions = torch.zeros((max_cycles, n_agents)).to(device) + rb_logprobs = torch.zeros((max_cycles, n_agents)).to(device) + rb_rewards = torch.zeros((max_cycles, n_agents)).to(device) + rb_terms = torch.zeros((max_cycles, n_agents)).to(device) + rb_values = torch.zeros((max_cycles, n_agents)).to(device) + + agent_tasks, env_tasks = [], [] + agent_c_rew, opp_c_rew = 0, 0 + n_ends, n_learner_wins = 0, 0 + info = {} + + """ TRAINING LOGIC """ + # train for n number of episodes + for episode in tqdm(range(total_episodes)): + # collect an episode + with torch.no_grad(): + # collect observations and convert to batch of torch tensors + env_task, agent_task = curriculum.sample() + + env_tasks.append(env_task[0]) + agent_tasks.append(agent_task) + + next_obs = env.reset(env_task) + # reset the episodic return + total_episodic_return = 0 + n_steps = 0 + + # each episode has num_steps + for step in range(0, max_cycles): + # rollover the observation + joint_obs = batchify(next_obs, device).squeeze() + agent_obs, opponent_obs = joint_obs + + # get action from the agent and the opponent + actions, logprobs, _, values = agent.get_action_and_value( + agent_obs, flatten_start_dim=0 + ) + + opponent = curriculum.get_opponent(info.get("agent_id", 0)).to(device) + opponent_action, *_ = opponent.get_action_and_value( + opponent_obs, flatten_start_dim=0 + ) + # execute the environment and log data + joint_actions = torch.tensor((actions, opponent_action)) + next_obs, rewards, terms, truncs, info = env.step( + unbatchify(joint_actions, env.possible_agents), device, agent_task + ) + + opp_reward = rewards["agent_1"] + if opp_reward != 0: + n_ends += 1 + curriculum.update_winrate(info["agent_id"], opp_reward) + if opp_reward == -1: + n_learner_wins += 1 + + # add to episode storage + rb_obs[step] = batchify(next_obs, device) + rb_rewards[step] = batchify(rewards, device) + rb_terms[step] = batchify(terms, device) + rb_actions[step] = joint_actions + rb_logprobs[step] = logprobs + rb_values[step] = values.flatten() + + # compute episodic return + total_episodic_return += rb_rewards[step].cpu().numpy() + + # if we reach termination or truncation, end + if any([terms[a] for a in terms]) or any([truncs[a] for a in truncs]): + end_step = step + break + + with torch.no_grad(): + next_value = agent.get_value( + torch.tensor(next_obs["agent_0"]), flatten_start_dim=0 + ) + rb_advantages = torch.zeros_like(rb_rewards).to(device) + last_gae_lam = 0 + for t in reversed(range(end_step)): + if t == end_step - 1: + next_non_terminal = 1.0 - rb_terms[t + 1] + next_values = next_value + else: + next_non_terminal = 1.0 - rb_terms[t + 1] + next_values = rb_values[t + 1] + delta = ( + rb_rewards[t] + + gamma * next_values * next_non_terminal + - rb_values[t] + ) + rb_advantages[t] = last_gae_lam = ( + delta + gamma * gae_lambda * next_non_terminal * last_gae_lam + ) + rb_returns = rb_advantages + rb_values + # convert our episodes to batch of individual transitions + b_obs = torch.flatten(rb_obs[:end_step], start_dim=0, end_dim=1) + b_logprobs = torch.flatten(rb_logprobs[:end_step], start_dim=0, end_dim=1) + b_actions = torch.flatten(rb_actions[:end_step], start_dim=0, end_dim=1) + b_returns = torch.flatten(rb_returns[:end_step], start_dim=0, end_dim=1) + b_values = torch.flatten(rb_values[:end_step], start_dim=0, end_dim=1) + b_advantages = torch.flatten(rb_advantages[:end_step], start_dim=0, end_dim=1) + + # Optimizing the policy and value network + b_index = np.arange(len(b_obs)) + clip_fracs = [] + for repeat in range(epochs): + # shuffle the indices we use to access the data + np.random.shuffle(b_index) + for start in range(0, len(b_obs), batch_size): + # select the indices we want to train on + end = start + batch_size + batch_index = b_index[start:end] + + _, newlogprob, entropy, value = agent.get_action_and_value( + b_obs[batch_index], b_actions.long()[batch_index] + ) + logratio = newlogprob - b_logprobs[batch_index] + ratio = logratio.exp() + + with torch.no_grad(): + # calculate approx_kl http://joschu.net/blog/kl-approx.html + old_approx_kl = (-logratio).mean() + approx_kl = ((ratio - 1) - logratio).mean() + clip_fracs += [ + ((ratio - 1.0).abs() > clip_coef).float().mean().item() + ] + + # normalize advantages + rb_advantages = b_advantages[batch_index] + rb_advantages = (rb_advantages - rb_advantages.mean()) / ( + rb_advantages.std() + 1e-8 + ) + + # Policy loss + pg_loss1 = -b_advantages[batch_index] * ratio + pg_loss2 = -b_advantages[batch_index] * torch.clamp( + ratio, 1 - clip_coef, 1 + clip_coef + ) + pg_loss = torch.max(pg_loss1, pg_loss2).mean() + + # Value loss + value = value.flatten() + v_loss_unclipped = (value - b_returns[batch_index]) ** 2 + v_clipped = b_values[batch_index] + torch.clamp( + value - b_values[batch_index], + -clip_coef, + clip_coef, + ) + v_loss_clipped = (v_clipped - b_returns[batch_index]) ** 2 + v_loss_max = torch.max(v_loss_unclipped, v_loss_clipped) + v_loss = 0.5 * v_loss_max.mean() + + entropy_loss = entropy.mean() + loss = pg_loss - ent_coef * entropy_loss + v_loss * vf_coef + + optimizer.zero_grad() + loss.backward() + optimizer.step() + + y_pred, y_true = b_values.cpu().numpy(), b_returns.cpu().numpy() + var_y = np.var(y_true) + explained_var = np.nan if var_y == 0 else 1 - np.var(y_true - y_pred) / var_y + + # update opponent + if episode % fsp_update_frequency == 0 and episode != 0: + curriculum.update_agent(agent) + + agent_c_rew += rewards["agent_0"] + opp_c_rew += rewards["agent_1"] + grid_size = env.level[3]["grid_size_selected"] + walls_percentage = env.level[3]["clutter_rate_selected"] + + writer.add_scalar("charts/steps_per_ep", end_step, episode) + writer.add_scalar("charts/agent_reward", agent_c_rew, episode) + writer.add_scalar("charts/opponent_reward", opp_c_rew, episode) + writer.add_scalar("charts/grid_size", grid_size, episode) + writer.add_scalar("charts/walls_percentage", walls_percentage, episode) + writer.add_scalar("losses/value_loss", v_loss.item(), episode) + writer.add_scalar("losses/policy_loss", pg_loss.item(), episode) + writer.add_scalar("losses/entropy", entropy_loss.item(), episode) + writer.add_scalar("losses/old_approx_kl", old_approx_kl.item(), episode) + writer.add_scalar("losses/approx_kl", approx_kl.item(), episode) + + learner_winrate = n_learner_wins / n_ends + wandb.run.summary["n_episodes"] = total_episodes + wandb.run.summary["learner_winrate"] = learner_winrate + writer.add_scalar("charts/learner_winrate", learner_winrate) + + # agent tasks + fig = px.histogram(agent_tasks, height=400) + fig.update_layout(bargap=0.2) + fig.update_layout(showlegend=False) + wandb.log({"charts/agent_tasks": wandb.Html(plotly.io.to_html(fig))}) + + # env tasks + fig = px.histogram(env_tasks, height=400) + fig.update_layout(bargap=0.2) + fig.update_layout(showlegend=False) + wandb.log({"charts/env_tasks": wandb.Html(plotly.io.to_html(fig))}) + + # win rates and replays + agent_ids = np.arange(curriculum.n_stored_agents) + values = list(curriculum.history.values()) + winrates = [i["winrate"] for i in values] + n_games = [i["n_games"] for i in values] + + fig = make_subplots(rows=2, cols=1, subplot_titles=("Win Rate", "Number of Games")) + fig.add_trace( + go.Bar(x=agent_ids, y=winrates, name="Win Rate", marker_color="blue"), + row=1, + col=1, + ) + fig.add_trace( + go.Bar(x=agent_ids, y=n_games, name="Number of Games", marker_color="orange"), + row=2, + col=1, + ) + + fig.update_yaxes(range=[0, 1], row=1, col=1) + fig.update_layout(showlegend=False) + wandb.log({"charts/opponent_winrates": wandb.Html(plotly.io.to_html(fig))}) + + writer.close() diff --git a/lasertag_selfplay/lasertag_sp_dr_adversarial.py b/lasertag_selfplay/lasertag_sp_dr_adversarial.py new file mode 100644 index 00000000..6f999e4b --- /dev/null +++ b/lasertag_selfplay/lasertag_sp_dr_adversarial.py @@ -0,0 +1,513 @@ +import argparse +import os +import sys +import time +from copy import deepcopy +from typing import Tuple, TypeVar + +import numpy as np +import plotly +import plotly.express as px +import torch +import torch.nn as nn +import torch.optim as optim +import wandb +from gymnasium import spaces +from torch.distributions.categorical import Categorical +from torch.utils.tensorboard import SummaryWriter +from tqdm.auto import tqdm + +sys.path.append("../") + +from lasertag import LasertagAdversarial # noqa: E402 +from syllabus.core import ( # noqa: E402 + Curriculum, + TaskWrapper, + make_multiprocessing_curriculum, +) +from syllabus.curricula import DomainRandomization # noqa: E402 +from syllabus.task_space import TaskSpace # noqa: E402 + +ObsType = TypeVar("ObsType") +ActionType = TypeVar("ActionType") +AgentID = TypeVar("AgentID") +Agent = TypeVar("Agent") +EnvTask = TypeVar("EnvTask") +AgentTask = TypeVar("AgentTask") + + +def parse_args(): + parser = argparse.ArgumentParser() + parser.add_argument("--track", type=bool, default=False) + parser.add_argument( + "--exp-name", + type=str, + default=os.path.basename(__file__).rstrip(".py"), + help="the name of this experiment", + ) + parser.add_argument( + "--wandb-project-name", + type=str, + default="syllabus", + help="the wandb's project name", + ) + parser.add_argument( + "--wandb-entity", + type=str, + default="rpegoud", + help="the entity (team) of wandb's project", + ) + parser.add_argument( + "--logging-dir", + type=str, + default=".", + help="the base directory for logging and wandb storage.", + ) + + args = parser.parse_args() + return args + + +def batchify(x, device): + """Converts PZ style returns to batch of torch arrays.""" + # convert to list of np arrays + x = np.stack([x[a] for a in x], axis=0) + # convert to torch + x = torch.tensor(x).to(device) + + return x + + +def unbatchify(x, possible_agents: np.ndarray): + """Converts np array to PZ style arguments.""" + x = x.cpu().numpy() + x = {agent: x[idx] for idx, agent in enumerate(possible_agents)} + + return x + + +class LasertagParallelWrapper(TaskWrapper): + """ + Wrapper ensuring compatibility with the PettingZoo Parallel API. + + Lasertag Environment: + * Action shape: `n_agents` * `Discrete(5)` + * Observation shape: Dict('image': Box(0, 255, (`n_agents`, 3, 5, 5), uint8)) + """ + + def __init__(self, n_agents, *args, **kwargs): + super().__init__(*args, **kwargs) + self.n_agents = n_agents + self.task = None + self.episode_return = 0 + # self.task_space = TaskSpace(spaces.MultiDiscrete(np.array([[2], [5]]))) + self.possible_agents = [f"agent_{i}" for i in range(self.n_agents)] + + def __getattr__(self, name): + """ + Delegate attribute lookup to the wrapped environment if the attribute + is not found in the LasertagParallelWrapper instance. + """ + return getattr(self.env, name) + + def _np_array_to_pz_dict(self, array: np.ndarray) -> dict[str : np.ndarray]: + """ + Returns a dictionary containing individual observations for each agent. + Assumes that the batch dimension represents individual agents. + """ + out = {} + for idx, value in enumerate(array): + out[self.possible_agents[idx]] = value + return out + + def _singleton_to_pz_dict(self, value: bool) -> dict[str:bool]: + """ + Broadcasts the `done` and `trunc` flags to dictionaries keyed by agent id. + """ + return {str(agent_index): value for agent_index in range(self.n_agents)} + + def reset( + self, env_task: int + ) -> tuple[dict[AgentID, ObsType], dict[AgentID, dict]]: + """ + Resets the environment and returns a dictionary of observations + keyed by agent ID. + """ + self.env.seed(env_task) + obs = self.env.reset_random() # random level generation + pz_obs = self._np_array_to_pz_dict(obs["image"]) + + return pz_obs + + def step( + self, action: dict[AgentID, ActionType], device: str, agent_task: int + ) -> tuple[ + dict[AgentID, ObsType], + dict[AgentID, float], + dict[AgentID, bool], + dict[AgentID, bool], + dict[AgentID, dict], + ]: + """ + Takes inputs in the PettingZoo (PZ) Parallel API format, performs a step and + returns outputs in PZ format. + """ + action = batchify(action, device) + obs, rew, done, info = self.env.step(action) + obs = obs["image"] + trunc = False # there is no `truncated` flag in this environment + self.task_completion = self._task_completion(obs, rew, done, trunc, info) + # convert outputs back to PZ format + obs, rew = map(self._np_array_to_pz_dict, [obs, rew]) + done, trunc, info = map( + self._singleton_to_pz_dict, [done, trunc, self.task_completion] + ) + info["agent_id"] = agent_task + + return self.observation(obs), rew, done, trunc, info + + +class SelfPlay(Curriculum): + def __init__(self, agent, device: str, store_agents_on_cpu: bool = False): + self.store_agents_on_cpu = store_agents_on_cpu + self.storage_device = "cpu" if self.store_agents_on_cpu else device + self.agent = deepcopy(agent).to(self.storage_device) + self.task_space = TaskSpace( + spaces.Discrete(1) + ) # SelfPlay can only return agent_id = 0 + + def update_agent(self, agent: Agent) -> Agent: + self.agent = deepcopy(agent).to(self.storage_device) + + def get_opponent(self, agent_id) -> tuple[Agent, int]: + if agent_id is None: + agent_id = 0 + assert ( + agent_id == 0 + ), f"Self play only tracks the current agent. Expected agent id 0, got {agent_id}" + return self.agent, 0 + + def sample(self, k=1): + return 0 + + +class Agent(nn.Module): + def __init__(self, num_actions): + super().__init__() + + self.network = nn.Sequential( + self._layer_init(nn.Linear(3 * 5 * 5, 512)), + nn.ReLU(), + ) + self.actor = self._layer_init(nn.Linear(512, num_actions), std=0.01) + self.critic = self._layer_init(nn.Linear(512, 1)) + + def _layer_init(self, layer, std=np.sqrt(2), bias_const=0.0): + torch.nn.init.orthogonal_(layer.weight, std) + torch.nn.init.constant_(layer.bias, bias_const) + return layer + + def get_value(self, x, flatten_start_dim=1): + x = torch.flatten(x, start_dim=flatten_start_dim) + return self.critic(self.network(x / 255.0)) + + def get_action_and_value(self, x, action=None, flatten_start_dim=1): + x = torch.flatten(x, start_dim=flatten_start_dim) + hidden = self.network(x / 255.0) + logits = self.actor(hidden) + probs = Categorical(logits=logits) + if action is None: + action = probs.sample() + return action, probs.log_prob(action), probs.entropy(), self.critic(hidden) + + +class DualCurriculumWrapper: + """Curriculum wrapper containing both an agent and environment-based curriculum.""" + + def __init__( + self, + env: TaskWrapper, + env_curriculum: Curriculum, + agent_curriculum: Curriculum, + *args, + **kwargs, + ) -> None: + super().__init__(*args, **kwargs) + self.env = env + self.agent_curriculum = agent_curriculum + self.env_curriculum = env_curriculum + + self.env_mp_curriculum, self.env_task_queue, self.env_update_queue = ( + make_multiprocessing_curriculum(env_curriculum) + ) + self.agent_mp_curriculum, self.agent_task_queue, self.agent_update_queue = ( + make_multiprocessing_curriculum(agent_curriculum) + ) + self.sample() # initializes env_task and agent_task + + def sample(self) -> Tuple[EnvTask, AgentTask]: + """Sets new tasks for the environment and agent curricula.""" + self.env_task = self.env_mp_curriculum.sample() + self.agent_task = self.agent_mp_curriculum.sample() + return self.env_task, self.agent_task + + def get_opponent(self, agent_task: AgentTask) -> Agent: + return self.agent_mp_curriculum.curriculum.get_opponent(agent_task) + + def update_agent(self, agent: Agent) -> Agent: + return self.agent_mp_curriculum.curriculum.update_agent(agent) + + def __getattr__(self, name): + """Delegate attribute lookup to the curricula if not found.""" + if hasattr(self.env_curriculum, name): + return getattr(self.env_curriculum, name) + elif hasattr(self.agent_curriculum, name): + return getattr(self.agent_curriculum, name) + else: + raise AttributeError( + f"'{self.__class__.__name__}' object has no attribute '{name}'" + ) + + +if __name__ == "__main__": + args = parse_args() + run_name = f"lasertag__{args.exp_name}__{int(time.time())}" + if args.track: + wandb.init( + project=args.wandb_project_name, + entity=args.wandb_entity, + sync_tensorboard=True, + config=vars(args), + name=run_name, + monitor_gym=True, + save_code=True, + dir=args.logging_dir, + ) + wandb.run.log_code(os.path.join(args.logging_dir, "/syllabus/examples")) + + writer = SummaryWriter(os.path.join(args.logging_dir, "./runs/{run_name}")) + writer.add_text( + "hyperparameters", + "|param|value|\n|-|-|\n%s" + % ("\n".join([f"|{key}|{value}|" for key, value in vars(args).items()])), + ) + + """ALGO PARAMS""" + device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + ent_coef = 0.0 + vf_coef = 0.5 + clip_coef = 0.2 + learning_rate = 1e-4 + epsilon = 1e-5 + gamma = 0.995 + gae_lambda = 0.95 + epochs = 5 + batch_size = 32 + stack_size = 3 + frame_size = (5, 5) + max_cycles = 201 # lasertag has 200 maximum steps by default + total_episodes = 500 + n_agents = 2 + num_actions = 5 + + """ LEARNER SETUP """ + agent = Agent(num_actions=num_actions).to(device) + optimizer = optim.Adam(agent.parameters(), lr=learning_rate, eps=epsilon) + + """ ENV SETUP """ + env = LasertagAdversarial(record_video=False) # 2 agents by default + env = LasertagParallelWrapper(env=env, n_agents=n_agents) + agent_curriculum = SelfPlay(agent=agent, device=device, store_agents_on_cpu=True) + observation_size = env.observation_space["image"].shape[1:] + env_curriculum = DomainRandomization(TaskSpace(spaces.Discrete(200))) + curriculum = DualCurriculumWrapper( + env=env, + agent_curriculum=agent_curriculum, + env_curriculum=env_curriculum, + ) + + """ ALGO LOGIC: EPISODE STORAGE""" + end_step = 0 + total_episodic_return = 0 + rb_obs = torch.zeros((max_cycles, n_agents, stack_size, *frame_size)).to(device) + rb_actions = torch.zeros((max_cycles, n_agents)).to(device) + rb_logprobs = torch.zeros((max_cycles, n_agents)).to(device) + rb_rewards = torch.zeros((max_cycles, n_agents)).to(device) + rb_terms = torch.zeros((max_cycles, n_agents)).to(device) + rb_values = torch.zeros((max_cycles, n_agents)).to(device) + + agent_tasks, env_tasks = [], [] + agent_c_rew, opp_c_rew = 0, 0 + info = {} + + """ TRAINING LOGIC """ + + # train for n number of episodes + for episode in tqdm(range(total_episodes)): + # collect an episode + with torch.no_grad(): + # collect observations and convert to batch of torch tensors + env_task, agent_task = curriculum.sample() + + env_tasks.append(env_task[0]) + agent_tasks.append(agent_task) + + next_obs = env.reset(env_task) + # reset the episodic return + total_episodic_return = 0 + n_steps = 0 + + # each episode has num_steps + for step in range(0, max_cycles): + # rollover the observation + joint_obs = batchify(next_obs, device).squeeze() + agent_obs, opponent_obs = joint_obs + + # get action from the agent and the opponent + actions, logprobs, _, values = agent.get_action_and_value( + agent_obs, flatten_start_dim=0 + ) + + opponent = curriculum.get_opponent(info.get("agent_id", 0)) + opponent_action, *_ = opponent.get_action_and_value( + opponent_obs, flatten_start_dim=0 + ) + # execute the environment and log data + joint_actions = torch.tensor((actions, opponent_action)) + next_obs, rewards, terms, truncs, info = env.step( + unbatchify(joint_actions, env.possible_agents), device, agent_task + ) + + # add to episode storage + rb_obs[step] = batchify(next_obs, device) + rb_rewards[step] = batchify(rewards, device) + rb_terms[step] = batchify(terms, device) + rb_actions[step] = joint_actions + rb_logprobs[step] = logprobs + rb_values[step] = values.flatten() + + # compute episodic return + total_episodic_return += rb_rewards[step].cpu().numpy() + + # if we reach termination or truncation, end + if any([terms[a] for a in terms]) or any([truncs[a] for a in truncs]): + end_step = step + break + + with torch.no_grad(): + next_value = agent.get_value( + torch.tensor(next_obs["agent_0"]), flatten_start_dim=0 + ) + rb_advantages = torch.zeros_like(rb_rewards).to(device) + last_gae_lam = 0 + for t in reversed(range(end_step)): + if t == end_step - 1: + next_non_terminal = 1.0 - rb_terms[t + 1] + next_values = next_value + else: + next_non_terminal = 1.0 - rb_terms[t + 1] + next_values = rb_values[t + 1] + delta = ( + rb_rewards[t] + + gamma * next_values * next_non_terminal + - rb_values[t] + ) + rb_advantages[t] = last_gae_lam = ( + delta + gamma * gae_lambda * next_non_terminal * last_gae_lam + ) + rb_returns = rb_advantages + rb_values + # convert our episodes to batch of individual transitions + b_obs = torch.flatten(rb_obs[:end_step], start_dim=0, end_dim=1) + b_logprobs = torch.flatten(rb_logprobs[:end_step], start_dim=0, end_dim=1) + b_actions = torch.flatten(rb_actions[:end_step], start_dim=0, end_dim=1) + b_returns = torch.flatten(rb_returns[:end_step], start_dim=0, end_dim=1) + b_values = torch.flatten(rb_values[:end_step], start_dim=0, end_dim=1) + b_advantages = torch.flatten(rb_advantages[:end_step], start_dim=0, end_dim=1) + + # Optimizing the policy and value network + b_index = np.arange(len(b_obs)) + clip_fracs = [] + for repeat in range(epochs): + # shuffle the indices we use to access the data + np.random.shuffle(b_index) + for start in range(0, len(b_obs), batch_size): + # select the indices we want to train on + end = start + batch_size + batch_index = b_index[start:end] + + _, newlogprob, entropy, value = agent.get_action_and_value( + b_obs[batch_index], b_actions.long()[batch_index] + ) + logratio = newlogprob - b_logprobs[batch_index] + ratio = logratio.exp() + + with torch.no_grad(): + # calculate approx_kl http://joschu.net/blog/kl-approx.html + old_approx_kl = (-logratio).mean() + approx_kl = ((ratio - 1) - logratio).mean() + clip_fracs += [ + ((ratio - 1.0).abs() > clip_coef).float().mean().item() + ] + + # normalize advantages + rb_advantages = b_advantages[batch_index] + rb_advantages = (rb_advantages - rb_advantages.mean()) / ( + rb_advantages.std() + 1e-8 + ) + + # Policy loss + pg_loss1 = -b_advantages[batch_index] * ratio + pg_loss2 = -b_advantages[batch_index] * torch.clamp( + ratio, 1 - clip_coef, 1 + clip_coef + ) + pg_loss = torch.max(pg_loss1, pg_loss2).mean() + + # Value loss + value = value.flatten() + v_loss_unclipped = (value - b_returns[batch_index]) ** 2 + v_clipped = b_values[batch_index] + torch.clamp( + value - b_values[batch_index], + -clip_coef, + clip_coef, + ) + v_loss_clipped = (v_clipped - b_returns[batch_index]) ** 2 + v_loss_max = torch.max(v_loss_unclipped, v_loss_clipped) + v_loss = 0.5 * v_loss_max.mean() + + entropy_loss = entropy.mean() + loss = pg_loss - ent_coef * entropy_loss + v_loss * vf_coef + + optimizer.zero_grad() + loss.backward() + optimizer.step() + + y_pred, y_true = b_values.cpu().numpy(), b_returns.cpu().numpy() + var_y = np.var(y_true) + explained_var = np.nan if var_y == 0 else 1 - np.var(y_true - y_pred) / var_y + + # update opponent + curriculum.update_agent(agent) + + agent_c_rew += rewards["agent_0"] + opp_c_rew += rewards["agent_1"] + grid_size = env.level[3]["grid_size_selected"] + walls_percentage = env.level[3]["clutter_rate_selected"] + + writer.add_scalar("charts/steps_per_ep", end_step, episode) + writer.add_scalar("charts/grid_size", grid_size, episode) + writer.add_scalar("charts/walls_percentage", walls_percentage, episode) + writer.add_scalar("losses/value_loss", v_loss.item(), episode) + writer.add_scalar("losses/policy_loss", pg_loss.item(), episode) + writer.add_scalar("losses/entropy", entropy_loss.item(), episode) + writer.add_scalar("losses/old_approx_kl", old_approx_kl.item(), episode) + writer.add_scalar("losses/approx_kl", approx_kl.item(), episode) + + fig = px.histogram(agent_tasks, height=400) + fig.update_layout(bargap=0.2) + wandb.log({"charts/agent_tasks": wandb.Html(plotly.io.to_html(fig))}) + + fig = px.histogram(env_tasks, height=400) + fig.update_layout(bargap=0.2) + wandb.log({"charts/env_tasks": wandb.Html(plotly.io.to_html(fig))}) + + writer.close() diff --git a/lasertag_selfplay/pfsp_dr.ipynb b/lasertag_selfplay/pfsp_dr.ipynb new file mode 100644 index 00000000..11d8365a --- /dev/null +++ b/lasertag_selfplay/pfsp_dr.ipynb @@ -0,0 +1,1544 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "import argparse\n", + "import os\n", + "import sys\n", + "import time\n", + "from typing import Tuple, TypeVar\n", + "import plotly.graph_objects as go\n", + "from plotly.subplots import make_subplots\n", + "import joblib\n", + "import numpy as np\n", + "import plotly\n", + "import plotly.express as px\n", + "import torch\n", + "import torch.nn as nn\n", + "import torch.optim as optim\n", + "import wandb\n", + "from gymnasium import spaces\n", + "from scipy.special import softmax\n", + "from torch.distributions.categorical import Categorical\n", + "from torch.utils.tensorboard import SummaryWriter\n", + "from tqdm.auto import tqdm\n", + "\n", + "sys.path.append(\"../\")\n", + "\n", + "from lasertag import LasertagAdversarial # noqa: E402\n", + "from syllabus.core import ( # noqa: E402\n", + " Curriculum,\n", + " TaskWrapper,\n", + " make_multiprocessing_curriculum,\n", + ")\n", + "from syllabus.curricula import DomainRandomization # noqa: E402\n", + "from syllabus.task_space import TaskSpace # noqa: E402\n", + "\n", + "ObsType = TypeVar(\"ObsType\")\n", + "ActionType = TypeVar(\"ActionType\")\n", + "AgentID = TypeVar(\"AgentID\")\n", + "Agent = TypeVar(\"Agent\")\n", + "EnvTask = TypeVar(\"EnvTask\")\n", + "AgentTask = TypeVar(\"AgentTask\")\n", + "\n", + "\n", + "def batchify(x, device):\n", + " \"\"\"Converts PZ style returns to batch of torch arrays.\"\"\"\n", + " # convert to list of np arrays\n", + " x = np.stack([x[a] for a in x], axis=0)\n", + " # convert to torch\n", + " x = torch.tensor(x).to(device)\n", + "\n", + " return x\n", + "\n", + "\n", + "def unbatchify(x, possible_agents: np.ndarray):\n", + " \"\"\"Converts np array to PZ style arguments.\"\"\"\n", + " x = x.cpu().numpy()\n", + " x = {agent: x[idx] for idx, agent in enumerate(possible_agents)}\n", + "\n", + " return x\n", + "\n", + "\n", + "class LasertagParallelWrapper(TaskWrapper):\n", + " \"\"\"\n", + " Wrapper ensuring compatibility with the PettingZoo Parallel API.\n", + "\n", + " Lasertag Environment:\n", + " * Action shape: `n_agents` * `Discrete(5)`\n", + " * Observation shape: Dict('image': Box(0, 255, (`n_agents`, 3, 5, 5), uint8))\n", + " \"\"\"\n", + "\n", + " def __init__(self, n_agents, *args, **kwargs):\n", + " super().__init__(*args, **kwargs)\n", + " self.n_agents = n_agents\n", + " self.task = None\n", + " self.episode_return = 0\n", + " # self.task_space = TaskSpace(spaces.MultiDiscrete(np.array([[2], [5]])))\n", + " self.possible_agents = [f\"agent_{i}\" for i in range(self.n_agents)]\n", + "\n", + " def __getattr__(self, name):\n", + " \"\"\"\n", + " Delegate attribute lookup to the wrapped environment if the attribute\n", + " is not found in the LasertagParallelWrapper instance.\n", + " \"\"\"\n", + " return getattr(self.env, name)\n", + "\n", + " def _np_array_to_pz_dict(self, array: np.ndarray) -> dict[str : np.ndarray]:\n", + " \"\"\"\n", + " Returns a dictionary containing individual observations for each agent.\n", + " Assumes that the batch dimension represents individual agents.\n", + " \"\"\"\n", + " out = {}\n", + " for idx, value in enumerate(array):\n", + " out[self.possible_agents[idx]] = value\n", + " return out\n", + "\n", + " def _singleton_to_pz_dict(self, value: bool) -> dict[str:bool]:\n", + " \"\"\"\n", + " Broadcasts the `done` and `trunc` flags to dictionaries keyed by agent id.\n", + " \"\"\"\n", + " return {str(agent_index): value for agent_index in range(self.n_agents)}\n", + "\n", + " def reset(\n", + " self, env_task: int\n", + " ) -> tuple[dict[AgentID, ObsType], dict[AgentID, dict]]:\n", + " \"\"\"\n", + " Resets the environment and returns a dictionary of observations\n", + " keyed by agent ID.\n", + " \"\"\"\n", + " self.env.seed(env_task)\n", + " obs = self.env.reset_random() # random level generation\n", + " pz_obs = self._np_array_to_pz_dict(obs[\"image\"])\n", + "\n", + " return pz_obs\n", + "\n", + " def step(\n", + " self, action: dict[AgentID, ActionType], device: str, agent_task: int\n", + " ) -> tuple[\n", + " dict[AgentID, ObsType],\n", + " dict[AgentID, float],\n", + " dict[AgentID, bool],\n", + " dict[AgentID, bool],\n", + " dict[AgentID, dict],\n", + " ]:\n", + " \"\"\"\n", + " Takes inputs in the PettingZoo (PZ) Parallel API format, performs a step and\n", + " returns outputs in PZ format.\n", + " \"\"\"\n", + " action = batchify(action, device)\n", + " obs, rew, done, info = self.env.step(action)\n", + " obs = obs[\"image\"]\n", + " trunc = False # there is no `truncated` flag in this environment\n", + " self.task_completion = self._task_completion(obs, rew, done, trunc, info)\n", + " # convert outputs back to PZ format\n", + " obs, rew = map(self._np_array_to_pz_dict, [obs, rew])\n", + " done, trunc, info = map(\n", + " self._singleton_to_pz_dict, [done, trunc, self.task_completion]\n", + " )\n", + " info[\"agent_id\"] = agent_task\n", + "\n", + " return self.observation(obs), rew, done, trunc, info\n", + "\n", + "\n", + "class PrioritizedFictitiousSelfPlay(Curriculum):\n", + " def __init__(\n", + " self,\n", + " agent,\n", + " storage_path: str,\n", + " max_agents: int,\n", + " ):\n", + " self.name = \"PFSP\"\n", + " self.storage_path = storage_path\n", + " if not os.path.exists(self.storage_path):\n", + " os.makedirs(self.storage_path, exist_ok=True)\n", + "\n", + " self.n_stored_agents = 0\n", + " self.max_agents = max_agents\n", + " self.task_space = TaskSpace(spaces.Discrete(self.max_agents))\n", + " self.update_agent(agent) # creates the initial opponent\n", + " self.history = {\n", + " i: {\n", + " \"winrate\": 0,\n", + " \"n_games\": 0,\n", + " }\n", + " for i in range(self.max_agents)\n", + " }\n", + "\n", + " def update_agent(self, agent) -> None:\n", + " \"\"\"\n", + " Saves the current agent instance to a pickle file and update\n", + " its priority.\n", + " \"\"\"\n", + " if self.n_stored_agents < self.max_agents:\n", + " # TODO: define the expected behaviour when the limit is exceeded\n", + " joblib.dump(\n", + " agent,\n", + " filename=f\"{self.storage_path}/{self.name}_agent_checkpoint_{self.n_stored_agents}.pkl\",\n", + " )\n", + " self.n_stored_agents += 1\n", + "\n", + " def update_winrate(self, opponent_id: int, opponent_reward: int) -> None:\n", + " \"\"\"\n", + " Uses an incremental mean to update the opponent's winrate i.e. priority.\n", + " This implies that sampling according to the winrates returns the most\n", + " challenging opponents.\n", + " \"\"\"\n", + " opponent_reward = opponent_reward > 0 # converts the reward to 0 or 1\n", + " self.history[opponent_id][\"n_games\"] += 1\n", + " old_winrate = self.history[opponent_id][\"winrate\"]\n", + " n = self.history[opponent_id][\"n_games\"]\n", + "\n", + " self.history[opponent_id][\"winrate\"] = (\n", + " old_winrate + (opponent_reward - old_winrate) / n\n", + " )\n", + "\n", + " def get_opponent(self, agent_id: int) -> Agent:\n", + " \"\"\"\n", + " Samples an agent id from the softmax distribution induced by winrates\n", + " then loads the selected agent from the buffer of saved agents.\n", + " \"\"\"\n", + " return joblib.load(\n", + " f\"{self.storage_path}/{self.name}_agent_checkpoint_{agent_id}.pkl\"\n", + " )\n", + " \n", + "\n", + " def sample(self, k=1):\n", + " logits = [\n", + " self.history[agent_id][\"winrate\"]\n", + " for agent_id in range(self.n_stored_agents)\n", + " ]\n", + " return np.random.choice(\n", + " np.arange(self.n_stored_agents),\n", + " p=softmax(logits),\n", + " )\n", + "\n", + "\n", + "class Agent(nn.Module):\n", + " def __init__(self, num_actions):\n", + " super().__init__()\n", + "\n", + " self.network = nn.Sequential(\n", + " self._layer_init(nn.Linear(3 * 5 * 5, 512)),\n", + " nn.ReLU(),\n", + " )\n", + " self.actor = self._layer_init(nn.Linear(512, num_actions), std=0.01)\n", + " self.critic = self._layer_init(nn.Linear(512, 1))\n", + "\n", + " def _layer_init(self, layer, std=np.sqrt(2), bias_const=0.0):\n", + " torch.nn.init.orthogonal_(layer.weight, std)\n", + " torch.nn.init.constant_(layer.bias, bias_const)\n", + " return layer\n", + "\n", + " def get_value(self, x, flatten_start_dim=1):\n", + " x = torch.flatten(x, start_dim=flatten_start_dim)\n", + " return self.critic(self.network(x / 255.0))\n", + "\n", + " def get_action_and_value(self, x, action=None, flatten_start_dim=1):\n", + " x = torch.flatten(x, start_dim=flatten_start_dim)\n", + " hidden = self.network(x / 255.0)\n", + " logits = self.actor(hidden)\n", + " probs = Categorical(logits=logits)\n", + " if action is None:\n", + " action = probs.sample()\n", + " return action, probs.log_prob(action), probs.entropy(), self.critic(hidden)\n", + "\n", + "\n", + "class DualCurriculumWrapper:\n", + " \"\"\"Curriculum wrapper containing both an agent and environment-based curriculum.\"\"\"\n", + "\n", + " def __init__(\n", + " self,\n", + " env: TaskWrapper,\n", + " env_curriculum: Curriculum,\n", + " agent_curriculum: Curriculum,\n", + " *args,\n", + " **kwargs,\n", + " ) -> None:\n", + " super().__init__(*args, **kwargs)\n", + " self.env = env\n", + " self.agent_curriculum = agent_curriculum\n", + " self.env_curriculum = env_curriculum\n", + "\n", + " self.env_mp_curriculum, self.env_task_queue, self.env_update_queue = (\n", + " make_multiprocessing_curriculum(env_curriculum)\n", + " )\n", + " self.agent_mp_curriculum, self.agent_task_queue, self.agent_update_queue = (\n", + " make_multiprocessing_curriculum(agent_curriculum)\n", + " )\n", + " self.sample() # initializes env_task and agent_task\n", + "\n", + " def sample(self) -> Tuple[EnvTask, AgentTask]:\n", + " \"\"\"Sets new tasks for the environment and agent curricula.\"\"\"\n", + " self.env_task = self.env_mp_curriculum.sample()\n", + " self.agent_task = self.agent_mp_curriculum.sample()\n", + " return self.env_task, self.agent_task\n", + "\n", + " def get_opponent(self, agent_task: AgentTask) -> Tuple[Agent, AgentTask]:\n", + " return self.agent_mp_curriculum.curriculum.get_opponent(agent_task)\n", + "\n", + " def update_agent(self, agent: Agent) -> Agent:\n", + " return self.agent_mp_curriculum.curriculum.update_agent(agent)\n", + "\n", + " def __getattr__(self, name):\n", + " \"\"\"Delegate attribute lookup to the curricula if not found.\"\"\"\n", + " if hasattr(self.env_curriculum, name):\n", + " return getattr(self.env_curriculum, name)\n", + " elif hasattr(self.agent_curriculum, name):\n", + " return getattr(self.agent_curriculum, name)\n", + " else:\n", + " raise AttributeError(\n", + " f\"'{self.__class__.__name__}' object has no attribute '{name}'\"\n", + " )\n" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "95b560569b7a4994815d488fef57c113", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + " 0%| | 0/10 [00:00 clip_coef).float().mean().item()\n", + " ]\n", + "\n", + " # normalize advantages\n", + " rb_advantages = b_advantages[batch_index]\n", + " rb_advantages = (rb_advantages - rb_advantages.mean()) / (\n", + " rb_advantages.std() + 1e-8\n", + " )\n", + "\n", + " # Policy loss\n", + " pg_loss1 = -b_advantages[batch_index] * ratio\n", + " pg_loss2 = -b_advantages[batch_index] * torch.clamp(\n", + " ratio, 1 - clip_coef, 1 + clip_coef\n", + " )\n", + " pg_loss = torch.max(pg_loss1, pg_loss2).mean()\n", + "\n", + " # Value loss\n", + " value = value.flatten()\n", + " v_loss_unclipped = (value - b_returns[batch_index]) ** 2\n", + " v_clipped = b_values[batch_index] + torch.clamp(\n", + " value - b_values[batch_index],\n", + " -clip_coef,\n", + " clip_coef,\n", + " )\n", + " v_loss_clipped = (v_clipped - b_returns[batch_index]) ** 2\n", + " v_loss_max = torch.max(v_loss_unclipped, v_loss_clipped)\n", + " v_loss = 0.5 * v_loss_max.mean()\n", + "\n", + " entropy_loss = entropy.mean()\n", + " loss = pg_loss - ent_coef * entropy_loss + v_loss * vf_coef\n", + "\n", + " optimizer.zero_grad()\n", + " loss.backward()\n", + " optimizer.step()\n", + "\n", + " y_pred, y_true = b_values.cpu().numpy(), b_returns.cpu().numpy()\n", + " var_y = np.var(y_true)\n", + " explained_var = np.nan if var_y == 0 else 1 - np.var(y_true - y_pred) / var_y\n", + "\n", + " # update opponent\n", + " if episode % fsp_update_frequency == 0 and episode != 0:\n", + " curriculum.update_agent(agent)\n", + "\n", + " agent_c_rew += rewards[\"agent_0\"]\n", + " opp_c_rew += rewards[\"agent_1\"]\n", + " grid_size = env.level[3][\"grid_size_selected\"]\n", + " walls_percentage = env.level[3][\"clutter_rate_selected\"]" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.plotly.v1+json": { + "config": { + "plotlyServerURL": "https://plotly.com" + }, + "data": [ + { + "marker": { + "color": "blue" + }, + "name": "Win Rate", + "type": "bar", + "x": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ], + "xaxis": "x", + "y": [ + 0.4666666666666667, + 0.125, + 0.33333333333333337, + 0.75, + 0, + 0.6666666666666666, + 0.8, + 1, + 0, + 0 + ], + "yaxis": "y" + }, + { + "marker": { + "color": "orange" + }, + "name": "Number of Games", + "type": "bar", + "x": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ], + "xaxis": "x2", + "y": [ + 15, + 8, + 3, + 8, + 1, + 3, + 5, + 2, + 0, + 1 + ], + "yaxis": "y2" + } + ], + "layout": { + "annotations": [ + { + "font": { + "size": 16 + }, + "showarrow": false, + "text": "Win Rate", + "x": 0.5, + "xanchor": "center", + "xref": "paper", + "y": 1, + "yanchor": "bottom", + "yref": "paper" + }, + { + "font": { + "size": 16 + }, + "showarrow": false, + "text": "Number of Games", + "x": 0.5, + "xanchor": "center", + "xref": "paper", + "y": 0.375, + "yanchor": "bottom", + "yref": "paper" + } + ], + "showlegend": false, + "template": { + "data": { + "bar": [ + { + "error_x": { + "color": "#2a3f5f" + }, + "error_y": { + "color": "#2a3f5f" + }, + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "bar" + } + ], + "barpolar": [ + { + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "barpolar" + } + ], + "carpet": [ + { + "aaxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "baxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "type": "carpet" + } + ], + "choropleth": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "choropleth" + } + ], + "contour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "contour" + } + ], + "contourcarpet": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "contourcarpet" + } + ], + "heatmap": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "heatmap" + } + ], + "heatmapgl": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "heatmapgl" + } + ], + "histogram": [ + { + "marker": { + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "histogram" + } + ], + "histogram2d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "histogram2d" + } + ], + "histogram2dcontour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "histogram2dcontour" + } + ], + "mesh3d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "mesh3d" + } + ], + "parcoords": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "parcoords" + } + ], + "pie": [ + { + "automargin": true, + "type": "pie" + } + ], + "scatter": [ + { + "fillpattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + }, + "type": "scatter" + } + ], + "scatter3d": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatter3d" + } + ], + "scattercarpet": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattercarpet" + } + ], + "scattergeo": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergeo" + } + ], + "scattergl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergl" + } + ], + "scattermapbox": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattermapbox" + } + ], + "scatterpolar": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolar" + } + ], + "scatterpolargl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolargl" + } + ], + "scatterternary": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterternary" + } + ], + "surface": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "surface" + } + ], + "table": [ + { + "cells": { + "fill": { + "color": "#EBF0F8" + }, + "line": { + "color": "white" + } + }, + "header": { + "fill": { + "color": "#C8D4E3" + }, + "line": { + "color": "white" + } + }, + "type": "table" + } + ] + }, + "layout": { + "annotationdefaults": { + "arrowcolor": "#2a3f5f", + "arrowhead": 0, + "arrowwidth": 1 + }, + "autotypenumbers": "strict", + "coloraxis": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "colorscale": { + "diverging": [ + [ + 0, + "#8e0152" + ], + [ + 0.1, + "#c51b7d" + ], + [ + 0.2, + "#de77ae" + ], + [ + 0.3, + "#f1b6da" + ], + [ + 0.4, + "#fde0ef" + ], + [ + 0.5, + "#f7f7f7" + ], + [ + 0.6, + "#e6f5d0" + ], + [ + 0.7, + "#b8e186" + ], + [ + 0.8, + "#7fbc41" + ], + [ + 0.9, + "#4d9221" + ], + [ + 1, + "#276419" + ] + ], + "sequential": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "sequentialminus": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ] + }, + "colorway": [ + "#636efa", + "#EF553B", + "#00cc96", + "#ab63fa", + "#FFA15A", + "#19d3f3", + "#FF6692", + "#B6E880", + "#FF97FF", + "#FECB52" + ], + "font": { + "color": "#2a3f5f" + }, + "geo": { + "bgcolor": "white", + "lakecolor": "white", + "landcolor": "#E5ECF6", + "showlakes": true, + "showland": true, + "subunitcolor": "white" + }, + "hoverlabel": { + "align": "left" + }, + "hovermode": "closest", + "mapbox": { + "style": "light" + }, + "paper_bgcolor": "white", + "plot_bgcolor": "#E5ECF6", + "polar": { + "angularaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "radialaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "scene": { + "xaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "yaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "zaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + } + }, + "shapedefaults": { + "line": { + "color": "#2a3f5f" + } + }, + "ternary": { + "aaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "baxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "caxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "title": { + "x": 0.05 + }, + "xaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + }, + "yaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + } + } + }, + "xaxis": { + "anchor": "y", + "domain": [ + 0, + 1 + ] + }, + "xaxis2": { + "anchor": "y2", + "domain": [ + 0, + 1 + ] + }, + "yaxis": { + "anchor": "x", + "domain": [ + 0.625, + 1 + ], + "range": [ + 0, + 1 + ] + }, + "yaxis2": { + "anchor": "x2", + "domain": [ + 0, + 0.375 + ] + } + } + } + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "agent_ids = np.arange(curriculum.n_stored_agents)\n", + "values = list(curriculum.history.values())\n", + "winrates = [i[\"winrate\"] for i in values]\n", + "n_games = [i[\"n_games\"] for i in values]\n", + "\n", + "fig = make_subplots(rows=2, cols=1, subplot_titles=(\"Win Rate\", \"Number of Games\"))\n", + "fig.add_trace(\n", + " go.Bar(x=agent_ids, y=winrates, name=\"Win Rate\", marker_color=\"blue\"), row=1, col=1\n", + ")\n", + "fig.add_trace(\n", + " go.Bar(x=agent_ids, y=n_games, name=\"Number of Games\", marker_color=\"orange\"),\n", + " row=2,\n", + " col=1,\n", + ")\n", + "\n", + "fig.update_yaxes(range=[0, 1], row=1, col=1)\n", + "fig.update_layout(showlegend=False)\n", + "fig.show()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.7" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/lasertag_selfplay/sp_dr.ipynb b/lasertag_selfplay/sp_dr.ipynb new file mode 100644 index 00000000..94fdb3ca --- /dev/null +++ b/lasertag_selfplay/sp_dr.ipynb @@ -0,0 +1,505 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import argparse\n", + "import os\n", + "import sys\n", + "import time\n", + "from copy import deepcopy\n", + "from typing import TypeVar,Any, Callable, Dict, Tuple\n", + "from gymnasium.utils.step_api_compatibility import step_api_compatibility\n", + "from multiprocessing import SimpleQueue\n", + "\n", + "import numpy as np\n", + "import torch\n", + "import torch.nn as nn\n", + "import torch.optim as optim\n", + "import wandb\n", + "from gymnasium import spaces\n", + "from torch.distributions.categorical import Categorical\n", + "from torch.utils.tensorboard import SummaryWriter\n", + "from tqdm.auto import tqdm\n", + "\n", + "sys.path.append(\"../\")\n", + "\n", + "from lasertag import LasertagAdversarial # noqa: E402\n", + "from syllabus.core import (\n", + " Curriculum,\n", + " TaskWrapper,\n", + " make_multiprocessing_curriculum,\n", + " MultiProcessingSyncWrapper,\n", + " MultiProcessingCurriculumWrapper,\n", + ") # noqa: E402\n", + "from syllabus.curricula import DomainRandomization # noqa: E402\n", + "from syllabus.task_space import TaskSpace # noqa: E402\n", + "\n", + "ObsType = TypeVar(\"ObsType\")\n", + "ActionType = TypeVar(\"ActionType\")\n", + "AgentID = TypeVar(\"AgentID\")\n", + "Agent = TypeVar(\"Agent\")\n", + "EnvTask = TypeVar(\"EnvTask\")\n", + "AgentTask = TypeVar(\"AgentTask\")\n", + "\n", + "\n", + "def batchify(x, device):\n", + " \"\"\"Converts PZ style returns to batch of torch arrays.\"\"\"\n", + " # convert to list of np arrays\n", + " x = np.stack([x[a] for a in x], axis=0)\n", + " # convert to torch\n", + " x = torch.tensor(x).to(device)\n", + "\n", + " return x\n", + "\n", + "\n", + "def unbatchify(x, possible_agents: np.ndarray):\n", + " \"\"\"Converts np array to PZ style arguments.\"\"\"\n", + " x = x.cpu().numpy()\n", + " x = {agent: x[idx] for idx, agent in enumerate(possible_agents)}\n", + "\n", + " return x\n", + "\n", + "\n", + "class LasertagParallelWrapper(TaskWrapper):\n", + " \"\"\"\n", + " Wrapper ensuring compatibility with the PettingZoo Parallel API.\n", + "\n", + " Lasertag Environment:\n", + " * Action shape: `n_agents` * `Discrete(5)`\n", + " * Observation shape: Dict('image': Box(0, 255, (`n_agents`, 3, 5, 5), uint8))\n", + " \"\"\"\n", + "\n", + " def __init__(self, n_agents, *args, **kwargs):\n", + " super().__init__(*args, **kwargs)\n", + " self.n_agents = n_agents\n", + " self.task = None\n", + " self.episode_return = 0\n", + " # self.task_space = TaskSpace(spaces.MultiDiscrete(np.array([[2], [5]])))\n", + " self.possible_agents = [f\"agent_{i}\" for i in range(self.n_agents)]\n", + "\n", + " def __getattr__(self, name):\n", + " \"\"\"\n", + " Delegate attribute lookup to the wrapped environment if the attribute\n", + " is not found in the LasertagParallelWrapper instance.\n", + " \"\"\"\n", + " return getattr(self.env, name)\n", + "\n", + " def _np_array_to_pz_dict(self, array: np.ndarray) -> dict[str : np.ndarray]:\n", + " \"\"\"\n", + " Returns a dictionary containing individual observations for each agent.\n", + " Assumes that the batch dimension represents individual agents.\n", + " \"\"\"\n", + " out = {}\n", + " for idx, value in enumerate(array):\n", + " out[self.possible_agents[idx]] = value\n", + " return out\n", + "\n", + " def _singleton_to_pz_dict(self, value: bool) -> dict[str:bool]:\n", + " \"\"\"\n", + " Broadcasts the `done` and `trunc` flags to dictionaries keyed by agent id.\n", + " \"\"\"\n", + " return {str(agent_index): value for agent_index in range(self.n_agents)}\n", + "\n", + " def reset(self, env_task:int) -> tuple[dict[AgentID, ObsType], dict[AgentID, dict]]:\n", + " \"\"\"\n", + " Resets the environment and returns a dictionary of observations\n", + " keyed by agent ID.\n", + " \"\"\"\n", + " self.env.seed(env_task)\n", + " obs = self.env.reset_random() # random level generation\n", + " pz_obs = self._np_array_to_pz_dict(obs[\"image\"])\n", + "\n", + " return pz_obs\n", + "\n", + " def step(\n", + " self, action: dict[AgentID, ActionType], device: str, agent_task: int\n", + " ) -> tuple[\n", + " dict[AgentID, ObsType],\n", + " dict[AgentID, float],\n", + " dict[AgentID, bool],\n", + " dict[AgentID, bool],\n", + " dict[AgentID, dict],\n", + " ]:\n", + " \"\"\"\n", + " Takes inputs in the PettingZoo (PZ) Parallel API format, performs a step and\n", + " returns outputs in PZ format.\n", + " \"\"\"\n", + " action = batchify(action, device)\n", + " obs, rew, done, info = self.env.step(action)\n", + " obs = obs[\"image\"]\n", + " trunc = False # there is no `truncated` flag in this environment\n", + " self.task_completion = self._task_completion(obs, rew, done, trunc, info)\n", + " # convert outputs back to PZ format\n", + " obs, rew = map(self._np_array_to_pz_dict, [obs, rew])\n", + " done, trunc, info = map(\n", + " self._singleton_to_pz_dict, [done, trunc, self.task_completion]\n", + " )\n", + " info[\"agent_id\"] = agent_task\n", + "\n", + " return self.observation(obs), rew, done, trunc, info\n", + "\n", + "\n", + "class SelfPlay(Curriculum):\n", + " def __init__(self, agent, device: str, store_agents_on_cpu: bool = False):\n", + " self.store_agents_on_cpu = store_agents_on_cpu\n", + " self.storage_device = \"cpu\" if self.store_agents_on_cpu else device\n", + " self.agent = deepcopy(agent).to(self.storage_device)\n", + " self.task_space = TaskSpace(\n", + " spaces.Discrete(1)\n", + " ) # SelfPlay can only return agent_id = 0\n", + "\n", + " def update_agent(self, agent: Agent) -> Agent:\n", + " self.agent = deepcopy(agent).to(self.storage_device)\n", + "\n", + " def get_opponent(self, agent_id) -> Agent:\n", + " if agent_id == None:\n", + " agent_id = 0\n", + " assert (\n", + " agent_id == 0\n", + " ), f\"Self play only tracks the current agent. Expected agent id 0, got {agent_id}\"\n", + " return self.agent\n", + "\n", + " def sample(self, k=1):\n", + " return 0\n", + "\n", + "\n", + "class Agent(nn.Module):\n", + " def __init__(self, num_actions):\n", + " super().__init__()\n", + "\n", + " self.network = nn.Sequential(\n", + " self._layer_init(nn.Linear(3 * 5 * 5, 512)),\n", + " nn.ReLU(),\n", + " )\n", + " self.actor = self._layer_init(nn.Linear(512, num_actions), std=0.01)\n", + " self.critic = self._layer_init(nn.Linear(512, 1))\n", + "\n", + " def _layer_init(self, layer, std=np.sqrt(2), bias_const=0.0):\n", + " torch.nn.init.orthogonal_(layer.weight, std)\n", + " torch.nn.init.constant_(layer.bias, bias_const)\n", + " return layer\n", + "\n", + " def get_value(self, x, flatten_start_dim=1):\n", + " x = torch.flatten(x, start_dim=flatten_start_dim)\n", + " return self.critic(self.network(x / 255.0))\n", + "\n", + " def get_action_and_value(self, x, action=None, flatten_start_dim=1):\n", + " x = torch.flatten(x, start_dim=flatten_start_dim)\n", + " hidden = self.network(x / 255.0)\n", + " logits = self.actor(hidden)\n", + " probs = Categorical(logits=logits)\n", + " if action is None:\n", + " action = probs.sample()\n", + " return action, probs.log_prob(action), probs.entropy(), self.critic(hidden)\n", + "\n", + "\n", + "if __name__ == \"__main__\":\n", + "\n", + " \"\"\"ALGO PARAMS\"\"\"\n", + " device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n", + " ent_coef = 0.0\n", + " vf_coef = 0.5\n", + " clip_coef = 0.2\n", + " learning_rate = 1e-4\n", + " epsilon = 1e-5\n", + " gamma = 0.995\n", + " gae_lambda = 0.95\n", + " epochs = 5\n", + " batch_size = 32\n", + " stack_size = 3\n", + " frame_size = (5, 5)\n", + " max_cycles = 201 # lasertag has 200 maximum steps by default\n", + " total_episodes = 20\n", + " n_agents = 2\n", + " num_actions = 5\n", + "\n", + " \"\"\" LEARNER SETUP \"\"\"\n", + " agent = Agent(num_actions=num_actions).to(device)\n", + " optimizer = optim.Adam(agent.parameters(), lr=learning_rate, eps=epsilon)\n", + "\n", + " \"\"\" ENV SETUP \"\"\"\n", + " env = LasertagAdversarial(record_video=False) # 2 agents by default\n", + " env = LasertagParallelWrapper(env=env, n_agents=n_agents)\n", + " curriculum = SelfPlay(agent=agent, device=device, store_agents_on_cpu=True)\n", + " observation_size = env.observation_space[\"image\"].shape[1:]\n", + "\n", + " \"\"\" ALGO LOGIC: EPISODE STORAGE\"\"\"\n", + " end_step = 0\n", + " total_episodic_return = 0\n", + " rb_obs = torch.zeros((max_cycles, n_agents, stack_size, *frame_size)).to(device)\n", + " rb_actions = torch.zeros((max_cycles, n_agents)).to(device)\n", + " rb_logprobs = torch.zeros((max_cycles, n_agents)).to(device)\n", + " rb_rewards = torch.zeros((max_cycles, n_agents)).to(device)\n", + " rb_terms = torch.zeros((max_cycles, n_agents)).to(device)\n", + " rb_values = torch.zeros((max_cycles, n_agents)).to(device)\n", + "\n", + " losses, episode_rewards = [], []\n", + " agent_c_rew, opp_c_rew = 0, 0\n", + " info = {}" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "class DualCurriculumWrapper:\n", + " \"\"\"Curriculum wrapper containing both an agent and environment-based curriculum.\"\"\"\n", + "\n", + " def __init__(\n", + " self,\n", + " env: TaskWrapper,\n", + " env_curriculum: Curriculum,\n", + " agent_curriculum: Curriculum,\n", + " *args,\n", + " **kwargs,\n", + " ) -> None:\n", + " super().__init__(*args, **kwargs)\n", + " self.env = env\n", + " self.agent_curriculum = agent_curriculum\n", + " self.env_curriculum = env_curriculum\n", + "\n", + " self.env_mp_curriculum, self.env_task_queue, self.env_update_queue = (\n", + " make_multiprocessing_curriculum(env_curriculum)\n", + " )\n", + " self.agent_mp_curriculum, self.agent_task_queue, self.agent_update_queue = (\n", + " make_multiprocessing_curriculum(agent_curriculum)\n", + " )\n", + " self.sample() # initializes env_task and agent_task\n", + "\n", + " def sample(self) -> Tuple[EnvTask, AgentTask]:\n", + " \"\"\"Sets new tasks for the environment and agent curricula.\"\"\"\n", + " self.env_task = self.env_mp_curriculum.sample()\n", + " self.agent_task = self.agent_mp_curriculum.sample()\n", + " return self.env_task, self.agent_task\n", + "\n", + " def get_opponent(self, agent_task: AgentTask) -> Tuple[Agent, AgentTask]:\n", + " return self.agent_mp_curriculum.curriculum.get_opponent(agent_task)\n", + "\n", + " def update_agent(self, agent: Agent) -> Agent:\n", + " return self.agent_mp_curriculum.curriculum.update_agent(agent)\n", + "\n", + " def __getattr__(self, name):\n", + " \"\"\"Delegate attribute lookup to the curricula if not found.\"\"\"\n", + " if hasattr(self.env_curriculum, name):\n", + " return getattr(self.env_curriculum, name)\n", + " elif hasattr(self.agent_curriculum, name):\n", + " return getattr(self.agent_curriculum, name)\n", + " else:\n", + " raise AttributeError(\n", + " f\"'{self.__class__.__name__}' object has no attribute '{name}'\"\n", + " )\n", + "\n", + "\n", + "agent_curriculum = SelfPlay(agent=agent, device=device, store_agents_on_cpu=True)\n", + "env_curriculum = DomainRandomization(TaskSpace(spaces.Discrete(200)))\n", + "curriculum = DualCurriculumWrapper(\n", + " env=env,\n", + " agent_curriculum=agent_curriculum,\n", + " env_curriculum=env_curriculum,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "931f7d9220f742ed920e6496e78c7e6b", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + " 0%| | 0/20 [00:00 clip_coef).float().mean().item()]\n", + "\n", + " # normalize advantages\n", + " rb_advantages = b_advantages[batch_index]\n", + " rb_advantages = (rb_advantages - rb_advantages.mean()) / (\n", + " rb_advantages.std() + 1e-8\n", + " )\n", + "\n", + " # Policy loss\n", + " pg_loss1 = -b_advantages[batch_index] * ratio\n", + " pg_loss2 = -b_advantages[batch_index] * torch.clamp(\n", + " ratio, 1 - clip_coef, 1 + clip_coef\n", + " )\n", + " pg_loss = torch.max(pg_loss1, pg_loss2).mean()\n", + "\n", + " # Value loss\n", + " value = value.flatten()\n", + " v_loss_unclipped = (value - b_returns[batch_index]) ** 2\n", + " v_clipped = b_values[batch_index] + torch.clamp(\n", + " value - b_values[batch_index],\n", + " -clip_coef,\n", + " clip_coef,\n", + " )\n", + " v_loss_clipped = (v_clipped - b_returns[batch_index]) ** 2\n", + " v_loss_max = torch.max(v_loss_unclipped, v_loss_clipped)\n", + " v_loss = 0.5 * v_loss_max.mean()\n", + "\n", + " entropy_loss = entropy.mean()\n", + " loss = pg_loss - ent_coef * entropy_loss + v_loss * vf_coef\n", + " losses.append(loss)\n", + "\n", + " optimizer.zero_grad()\n", + " loss.backward()\n", + " optimizer.step()\n", + "\n", + " y_pred, y_true = b_values.cpu().numpy(), b_returns.cpu().numpy()\n", + " var_y = np.var(y_true)\n", + " explained_var = np.nan if var_y == 0 else 1 - np.var(y_true - y_pred) / var_y\n", + "\n", + " # update opponent\n", + " curriculum.update_agent(agent)\n", + "\n", + " agent_c_rew += rewards[\"agent_0\"]\n", + " opp_c_rew += rewards[\"agent_1\"]\n", + " grid_size = env.level[3][\"grid_size_selected\"]\n", + " walls_percentage = env.level[3][\"clutter_rate_selected\"]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.7" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/ppo_atari.py b/ppo_atari.py deleted file mode 100644 index f6b2e5bc..00000000 --- a/ppo_atari.py +++ /dev/null @@ -1,372 +0,0 @@ -# docs and experiment results can be found at https://docs.cleanrl.dev/rl-algorithms/ppo/#ppo_atari_lstmpy -import argparse -import os -import random -import time -from distutils.util import strtobool - -import gym -import numpy as np -import torch -import torch.nn as nn -import torch.optim as optim -from torch.distributions.categorical import Categorical -from torch.utils.tensorboard import SummaryWriter - -from stable_baselines3.common.atari_wrappers import ( # isort:skip - ClipRewardEnv, - EpisodicLifeEnv, - FireResetEnv, - MaxAndSkipEnv, - NoopResetEnv, -) - - -def parse_args(): - # fmt: off - parser = argparse.ArgumentParser() - parser.add_argument("--exp-name", type=str, default=os.path.basename(__file__).rstrip(".py"), - help="the name of this experiment") - parser.add_argument("--seed", type=int, default=1, - help="seed of the experiment") - parser.add_argument("--torch-deterministic", type=lambda x: bool(strtobool(x)), default=True, nargs="?", const=True, - help="if toggled, `torch.backends.cudnn.deterministic=False`") - parser.add_argument("--cuda", type=lambda x: bool(strtobool(x)), default=True, nargs="?", const=True, - help="if toggled, cuda will be enabled by default") - parser.add_argument("--track", type=lambda x: bool(strtobool(x)), default=False, nargs="?", const=True, - help="if toggled, this experiment will be tracked with Weights and Biases") - parser.add_argument("--wandb-project-name", type=str, default="cleanRL", - help="the wandb's project name") - parser.add_argument("--wandb-entity", type=str, default=None, - help="the entity (team) of wandb's project") - parser.add_argument("--capture-video", type=lambda x: bool(strtobool(x)), default=False, nargs="?", const=True, - help="whether to capture videos of the agent performances (check out `videos` folder)") - - # Algorithm specific arguments - parser.add_argument("--env-id", type=str, default="BreakoutNoFrameskip-v4", - help="the id of the environment") - parser.add_argument("--total-timesteps", type=int, default=10000000, - help="total timesteps of the experiments") - parser.add_argument("--learning-rate", type=float, default=2.5e-4, - help="the learning rate of the optimizer") - parser.add_argument("--num-envs", type=int, default=8, - help="the number of parallel game environments") - parser.add_argument("--num-steps", type=int, default=128, - help="the number of steps to run in each environment per policy rollout") - parser.add_argument("--anneal-lr", type=lambda x: bool(strtobool(x)), default=True, nargs="?", const=True, - help="Toggle learning rate annealing for policy and value networks") - parser.add_argument("--gamma", type=float, default=0.99, - help="the discount factor gamma") - parser.add_argument("--gae-lambda", type=float, default=0.95, - help="the lambda for the general advantage estimation") - parser.add_argument("--num-minibatches", type=int, default=4, - help="the number of mini-batches") - parser.add_argument("--update-epochs", type=int, default=4, - help="the K epochs to update the policy") - parser.add_argument("--norm-adv", type=lambda x: bool(strtobool(x)), default=True, nargs="?", const=True, - help="Toggles advantages normalization") - parser.add_argument("--clip-coef", type=float, default=0.1, - help="the surrogate clipping coefficient") - parser.add_argument("--clip-vloss", type=lambda x: bool(strtobool(x)), default=True, nargs="?", const=True, - help="Toggles whether or not to use a clipped loss for the value function, as per the paper.") - parser.add_argument("--ent-coef", type=float, default=0.01, - help="coefficient of the entropy") - parser.add_argument("--vf-coef", type=float, default=0.5, - help="coefficient of the value function") - parser.add_argument("--max-grad-norm", type=float, default=0.5, - help="the maximum norm for the gradient clipping") - parser.add_argument("--target-kl", type=float, default=None, - help="the target KL divergence threshold") - args = parser.parse_args() - args.batch_size = int(args.num_envs * args.num_steps) - args.minibatch_size = int(args.batch_size // args.num_minibatches) - # fmt: on - return args - - -def make_env(env_id, seed, idx, capture_video, run_name): - def thunk(): - env = gym.make(env_id) - env = gym.wrappers.RecordEpisodeStatistics(env) - if capture_video: - if idx == 0: - env = gym.wrappers.RecordVideo(env, f"videos/{run_name}") - env = NoopResetEnv(env, noop_max=30) - env = MaxAndSkipEnv(env, skip=4) - env = EpisodicLifeEnv(env) - if "FIRE" in env.unwrapped.get_action_meanings(): - env = FireResetEnv(env) - env = ClipRewardEnv(env) - env = gym.wrappers.ResizeObservation(env, (84, 84)) - env = gym.wrappers.GrayScaleObservation(env) - env = gym.wrappers.FrameStack(env, 1) - env.seed(seed) - env.action_space.seed(seed) - env.observation_space.seed(seed) - return env - - return thunk - - -def layer_init(layer, std=np.sqrt(2), bias_const=0.0): - torch.nn.init.orthogonal_(layer.weight, std) - torch.nn.init.constant_(layer.bias, bias_const) - return layer - - -class Agent(nn.Module): - def __init__(self, envs): - super().__init__() - self.network = nn.Sequential( - layer_init(nn.Conv2d(1, 32, 8, stride=4)), - nn.ReLU(), - layer_init(nn.Conv2d(32, 64, 4, stride=2)), - nn.ReLU(), - layer_init(nn.Conv2d(64, 64, 3, stride=1)), - nn.ReLU(), - nn.Flatten(), - layer_init(nn.Linear(64 * 7 * 7, 512)), - nn.ReLU(), - ) - self.lstm = nn.LSTM(512, 128) - for name, param in self.lstm.named_parameters(): - if "bias" in name: - nn.init.constant_(param, 0) - elif "weight" in name: - nn.init.orthogonal_(param, 1.0) - self.actor = layer_init(nn.Linear(128, envs.single_action_space.n), std=0.01) - self.critic = layer_init(nn.Linear(128, 1), std=1) - - def get_states(self, x, lstm_state, done): - hidden = self.network(x / 255.0) - - # LSTM logic - batch_size = lstm_state[0].shape[1] - hidden = hidden.reshape((-1, batch_size, self.lstm.input_size)) - done = done.reshape((-1, batch_size)) - new_hidden = [] - for h, d in zip(hidden, done): - h, lstm_state = self.lstm( - h.unsqueeze(0), - ( - (1.0 - d).view(1, -1, 1) * lstm_state[0], - (1.0 - d).view(1, -1, 1) * lstm_state[1], - ), - ) - new_hidden += [h] - new_hidden = torch.flatten(torch.cat(new_hidden), 0, 1) - return new_hidden, lstm_state - - def get_value(self, x, lstm_state, done): - hidden, _ = self.get_states(x, lstm_state, done) - return self.critic(hidden) - - def get_action_and_value(self, x, lstm_state, done, action=None): - hidden, lstm_state = self.get_states(x, lstm_state, done) - logits = self.actor(hidden) - probs = Categorical(logits=logits) - if action is None: - action = probs.sample() - return action, probs.log_prob(action), probs.entropy(), self.critic(hidden), lstm_state - - -if __name__ == "__main__": - args = parse_args() - run_name = f"{args.env_id}__{args.exp_name}__{args.seed}__{int(time.time())}" - if args.track: - import wandb - - wandb.init( - project=args.wandb_project_name, - entity=args.wandb_entity, - sync_tensorboard=True, - config=vars(args), - name=run_name, - monitor_gym=True, - save_code=True, - ) - writer = SummaryWriter(f"runs/{run_name}") - writer.add_text( - "hyperparameters", - "|param|value|\n|-|-|\n%s" % ("\n".join([f"|{key}|{value}|" for key, value in vars(args).items()])), - ) - - # TRY NOT TO MODIFY: seeding - random.seed(args.seed) - np.random.seed(args.seed) - torch.manual_seed(args.seed) - torch.backends.cudnn.deterministic = args.torch_deterministic - - device = torch.device("cuda" if torch.cuda.is_available() and args.cuda else "cpu") - - # env setup - envs = gym.vector.SyncVectorEnv( - [make_env(args.env_id, args.seed + i, i, args.capture_video, run_name) for i in range(args.num_envs)] - ) - assert isinstance(envs.single_action_space, gym.spaces.Discrete), "only discrete action space is supported" - - agent = Agent(envs).to(device) - optimizer = optim.Adam(agent.parameters(), lr=args.learning_rate, eps=1e-5) - - # ALGO Logic: Storage setup - obs = torch.zeros((args.num_steps, args.num_envs) + envs.single_observation_space.shape).to(device) - actions = torch.zeros((args.num_steps, args.num_envs) + envs.single_action_space.shape).to(device) - logprobs = torch.zeros((args.num_steps, args.num_envs)).to(device) - rewards = torch.zeros((args.num_steps, args.num_envs)).to(device) - dones = torch.zeros((args.num_steps, args.num_envs)).to(device) - values = torch.zeros((args.num_steps, args.num_envs)).to(device) - - # TRY NOT TO MODIFY: start the game - global_step = 0 - start_time = time.time() - next_obs = torch.Tensor(envs.reset()).to(device) - next_done = torch.zeros(args.num_envs).to(device) - next_lstm_state = ( - torch.zeros(agent.lstm.num_layers, args.num_envs, agent.lstm.hidden_size).to(device), - torch.zeros(agent.lstm.num_layers, args.num_envs, agent.lstm.hidden_size).to(device), - ) # hidden and cell states (see https://youtu.be/8HyCNIVRbSU) - num_updates = args.total_timesteps // args.batch_size - - for update in range(1, num_updates + 1): - initial_lstm_state = (next_lstm_state[0].clone(), next_lstm_state[1].clone()) - # Annealing the rate if instructed to do so. - if args.anneal_lr: - frac = 1.0 - (update - 1.0) / num_updates - lrnow = frac * args.learning_rate - optimizer.param_groups[0]["lr"] = lrnow - - for step in range(0, args.num_steps): - global_step += 1 * args.num_envs - obs[step] = next_obs - dones[step] = next_done - - # ALGO LOGIC: action logic - with torch.no_grad(): - action, logprob, _, value, next_lstm_state = agent.get_action_and_value(next_obs, next_lstm_state, next_done) - values[step] = value.flatten() - actions[step] = action - logprobs[step] = logprob - - # TRY NOT TO MODIFY: execute the game and log data. - next_obs, reward, done, info = envs.step(action.cpu().numpy()) - rewards[step] = torch.tensor(reward).to(device).view(-1) - next_obs, next_done = torch.Tensor(next_obs).to(device), torch.Tensor(done).to(device) - - for item in info: - if "episode" in item.keys(): - print(f"global_step={global_step}, episodic_return={item['episode']['r']}") - writer.add_scalar("charts/episodic_return", item["episode"]["r"], global_step) - writer.add_scalar("charts/episodic_length", item["episode"]["l"], global_step) - break - - # bootstrap value if not done - with torch.no_grad(): - next_value = agent.get_value( - next_obs, - next_lstm_state, - next_done, - ).reshape(1, -1) - advantages = torch.zeros_like(rewards).to(device) - lastgaelam = 0 - for t in reversed(range(args.num_steps)): - if t == args.num_steps - 1: - nextnonterminal = 1.0 - next_done - nextvalues = next_value - else: - nextnonterminal = 1.0 - dones[t + 1] - nextvalues = values[t + 1] - delta = rewards[t] + args.gamma * nextvalues * nextnonterminal - values[t] - advantages[t] = lastgaelam = delta + args.gamma * args.gae_lambda * nextnonterminal * lastgaelam - returns = advantages + values - - # flatten the batch - b_obs = obs.reshape((-1,) + envs.single_observation_space.shape) - b_logprobs = logprobs.reshape(-1) - b_actions = actions.reshape((-1,) + envs.single_action_space.shape) - b_dones = dones.reshape(-1) - b_advantages = advantages.reshape(-1) - b_returns = returns.reshape(-1) - b_values = values.reshape(-1) - - # Optimizing the policy and value network - assert args.num_envs % args.num_minibatches == 0 - envsperbatch = args.num_envs // args.num_minibatches - envinds = np.arange(args.num_envs) - flatinds = np.arange(args.batch_size).reshape(args.num_steps, args.num_envs) - clipfracs = [] - for epoch in range(args.update_epochs): - np.random.shuffle(envinds) - for start in range(0, args.num_envs, envsperbatch): - end = start + envsperbatch - mbenvinds = envinds[start:end] - mb_inds = flatinds[:, mbenvinds].ravel() # be really careful about the index - - _, newlogprob, entropy, newvalue, _ = agent.get_action_and_value( - b_obs[mb_inds], - (initial_lstm_state[0][:, mbenvinds], initial_lstm_state[1][:, mbenvinds]), - b_dones[mb_inds], - b_actions.long()[mb_inds], - ) - logratio = newlogprob - b_logprobs[mb_inds] - ratio = logratio.exp() - - with torch.no_grad(): - # calculate approx_kl http://joschu.net/blog/kl-approx.html - old_approx_kl = (-logratio).mean() - approx_kl = ((ratio - 1) - logratio).mean() - clipfracs += [((ratio - 1.0).abs() > args.clip_coef).float().mean().item()] - - mb_advantages = b_advantages[mb_inds] - if args.norm_adv: - mb_advantages = (mb_advantages - mb_advantages.mean()) / (mb_advantages.std() + 1e-8) - - # Policy loss - pg_loss1 = -mb_advantages * ratio - pg_loss2 = -mb_advantages * torch.clamp(ratio, 1 - args.clip_coef, 1 + args.clip_coef) - pg_loss = torch.max(pg_loss1, pg_loss2).mean() - - # Value loss - newvalue = newvalue.view(-1) - if args.clip_vloss: - v_loss_unclipped = (newvalue - b_returns[mb_inds]) ** 2 - v_clipped = b_values[mb_inds] + torch.clamp( - newvalue - b_values[mb_inds], - -args.clip_coef, - args.clip_coef, - ) - v_loss_clipped = (v_clipped - b_returns[mb_inds]) ** 2 - v_loss_max = torch.max(v_loss_unclipped, v_loss_clipped) - v_loss = 0.5 * v_loss_max.mean() - else: - v_loss = 0.5 * ((newvalue - b_returns[mb_inds]) ** 2).mean() - - entropy_loss = entropy.mean() - loss = pg_loss - args.ent_coef * entropy_loss + v_loss * args.vf_coef - - optimizer.zero_grad() - loss.backward() - nn.utils.clip_grad_norm_(agent.parameters(), args.max_grad_norm) - optimizer.step() - - if args.target_kl is not None: - if approx_kl > args.target_kl: - break - - y_pred, y_true = b_values.cpu().numpy(), b_returns.cpu().numpy() - var_y = np.var(y_true) - explained_var = np.nan if var_y == 0 else 1 - np.var(y_true - y_pred) / var_y - - # TRY NOT TO MODIFY: record rewards for plotting purposes - writer.add_scalar("charts/learning_rate", optimizer.param_groups[0]["lr"], global_step) - writer.add_scalar("losses/value_loss", v_loss.item(), global_step) - writer.add_scalar("losses/policy_loss", pg_loss.item(), global_step) - writer.add_scalar("losses/entropy", entropy_loss.item(), global_step) - writer.add_scalar("losses/old_approx_kl", old_approx_kl.item(), global_step) - writer.add_scalar("losses/approx_kl", approx_kl.item(), global_step) - writer.add_scalar("losses/clipfrac", np.mean(clipfracs), global_step) - writer.add_scalar("losses/explained_variance", explained_var, global_step) - print("SPS:", int(global_step / (time.time() - start_time))) - writer.add_scalar("charts/SPS", int(global_step / (time.time() - start_time)), global_step) - - envs.close() - writer.close() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 2ced7c48..a7a8a1e3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ -#pettingzoo==1.19.0 -#supersuit==3.5.0 +# pettingzoo==1.19.0 +# supersuit==3.5.0 gym==0.23.1 numpy==1.23.3 wandb>=0.15.3 @@ -13,5 +13,5 @@ pytest>=8.1.1 pytest-benchmark>=3.4.1 # documentation -pip install sphinx-tabs +# pip install sphinx-tabs diff --git a/setup.py b/setup.py index 31e09f23..88614563 100644 --- a/setup.py +++ b/setup.py @@ -1,26 +1,36 @@ from setuptools import find_packages, setup - extras = dict() -extras['test'] = ['cmake', 'ninja', 'nle>=0.9.0', 'matplotlib>=3.7.1', 'scipy==1.10.0', 'tensorboard>=2.13.0', 'shimmy'] -extras['docs'] = ['sphinx-tabs', 'sphinxcontrib-spelling', 'furo'] -extras['all'] = extras['test'] + extras['docs'] +extras["test"] = [ + "cmake", + "ninja", + "nle>=0.9.0", + "matplotlib>=3.7.1", + "scipy==1.10.0", + "tensorboard>=2.13.0", + "shimmy", + "joblib==1.2.0", + "tqdm==4.66.1", + "griddly==1.6.7", + "plotly==5.18.0", +] +extras["docs"] = ["sphinx-tabs", "sphinxcontrib-spelling", "furo"] +extras["all"] = extras["test"] + extras["docs"] setup( name="syllabus-rl", - description="Syllabus Library" - "Curriculum learning tools for RL", + description="Syllabus Library" "Curriculum learning tools for RL", long_description_content_type="text/markdown", version="0.5", packages=find_packages(), include_package_data=True, install_requires=[ - 'pytest>=8.1.1', - 'pytest-benchmark>=3.4.1', - 'gymnasium>=0.28.0', - 'numpy>=1.24.0', - 'ray[rllib]>=2.8.1', - 'torch>=2.0.1', + "pytest>=8.1.1", + "pytest-benchmark>=3.4.1", + "gymnasium>=0.28.0", + "numpy>=1.24.0", + "ray[rllib]>=2.8.1", + "torch>=2.0.1", ], extras_require=extras, python_requires=">=3.8, <=3.11", @@ -35,6 +45,5 @@ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", - ], ) diff --git a/syllabus/core/__init__.py b/syllabus/core/__init__.py index 4657e775..43820ec7 100644 --- a/syllabus/core/__init__.py +++ b/syllabus/core/__init__.py @@ -1,3 +1,4 @@ +# flake8: noqa: F401 # Environment Code from .task_interface import TaskWrapper, SubclassTaskWrapper, ReinitTaskWrapper, PettingZooReinitTaskWrapper, TaskEnv, PettingZooTaskWrapper, PettingZooTaskEnv @@ -7,7 +8,6 @@ from .curriculum_sync_wrapper import (CurriculumWrapper, MultiProcessingComponents, MultiProcessingCurriculumWrapper, - MultiProcessingComponents, RayCurriculumWrapper, make_multiprocessing_curriculum, make_ray_curriculum) @@ -16,3 +16,4 @@ from .multivariate_curriculum_wrapper import MultitaskWrapper from .multiagent_curriculum_wrappers import MultiagentSharedCurriculumWrapper, MultiagentIndependentCurriculumWrapper from .stat_recorder import StatRecorder +from .dual_curriculum_wrapper import DualCurriculumWrapper diff --git a/syllabus/core/curriculum_base.py b/syllabus/core/curriculum_base.py index 4b027268..b0428caf 100644 --- a/syllabus/core/curriculum_base.py +++ b/syllabus/core/curriculum_base.py @@ -11,8 +11,7 @@ # TODO: Move non-generic logic to Uniform class. Allow subclasses to call super for generic error handling class Curriculum: - """Base class and API for defining curricula to interface with Gym environments. - """ + """Base class and API for defining curricula to interface with Gym environments.""" def __init__(self, task_space: TaskSpace, random_start_tasks: int = 0, task_names: Callable = None, record_stats: bool = False) -> None: """Initialize the base Curriculum @@ -23,7 +22,9 @@ def __init__(self, task_space: TaskSpace, random_start_tasks: int = 0, task_name TODO: Use task space for this :param task_names: Names of the tasks in the task space, defaults to None """ - assert isinstance(task_space, TaskSpace), f"task_space must be a TaskSpace object. Got {type(task_space)} instead." + assert isinstance( + task_space, TaskSpace + ), f"task_space must be a TaskSpace object. Got {type(task_space)} instead." self.task_space = task_space self.random_start_tasks = random_start_tasks self.completed_tasks = 0 @@ -32,7 +33,9 @@ def __init__(self, task_space: TaskSpace, random_start_tasks: int = 0, task_name self.stat_recorder = StatRecorder(self.task_space, task_names=task_names) if record_stats else None if self.num_tasks == 0: - warnings.warn("Task space is empty. This will cause errors during sampling if no tasks are added.") + warnings.warn( + "Task space is empty. This will cause errors during sampling if no tasks are added." + ) @property def requires_step_updates(self) -> bool: @@ -68,9 +71,13 @@ def tasks(self) -> List[tuple]: def add_task(self, task: typing.Any) -> None: # TODO - raise NotImplementedError("This curriculum does not support adding tasks after initialization.") + raise NotImplementedError( + "This curriculum does not support adding tasks after initialization." + ) - def update_task_progress(self, task: typing.Any, progress: Tuple[float, bool], env_id: int = None) -> None: + def update_task_progress( + self, task: typing.Any, progress: Tuple[float, bool], env_id: int = None + ) -> None: """Update the curriculum with a task and its progress. :param task: Task for which progress is being updated. @@ -79,8 +86,17 @@ def update_task_progress(self, task: typing.Any, progress: Tuple[float, bool], e self.completed_tasks += 1 - def update_on_step(self, task: typing.Any, obs: typing.Any, rew: float, term: bool, trunc: bool, info: dict, env_id: int = None) -> None: - """ Update the curriculum with the current step results from the environment. + def update_on_step( + self, + task: typing.Any, + obs: typing.Any, + rew: float, + term: bool, + trunc: bool, + info: dict, + env_id: int = None, + ) -> None: + """Update the curriculum with the current step results from the environment. :param obs: Observation from teh environment :param rew: Reward from the environment @@ -89,9 +105,15 @@ def update_on_step(self, task: typing.Any, obs: typing.Any, rew: float, term: bo :param info: Extra information from the environment :raises NotImplementedError: """ - raise NotImplementedError("This curriculum does not require step updates. Set update_on_step for the environment sync wrapper to False to improve performance and prevent this error.") - - def update_on_step_batch(self, step_results: List[typing.Tuple[Any, Any, int, int, int, int]], env_id: int = None) -> None: + raise NotImplementedError( + "This curriculum does not require step updates. Set update_on_step for the environment sync wrapper to False to improve performance and prevent this error." + ) + + def update_on_step_batch( + self, + step_results: List[typing.Tuple[Any, Any, int, int, int, int]], + env_id: int = None, + ) -> None: """Update the curriculum with a batch of step results from the environment. This method can be overridden to provide a more efficient implementation. It is used @@ -101,9 +123,17 @@ def update_on_step_batch(self, step_results: List[typing.Tuple[Any, Any, int, in """ tasks, obs, rews, terms, truncs, infos = tuple(step_results) for i in range(len(obs)): - self.update_on_step(tasks[i], obs[i], rews[i], terms[i], truncs[i], infos[i], env_id=env_id) - - def update_on_episode(self, episode_return: float, episode_length: int, episode_task: Any, env_id: int = None) -> None: + self.update_on_step( + tasks[i], obs[i], rews[i], terms[i], truncs[i], infos[i], env_id=env_id + ) + + def update_on_episode( + self, + episode_return: float, + episode_length: int, + episode_task: Any, + env_id: int = None, + ) -> None: """Update the curriculum with episode results from the environment. :param episode_return: Episodic return @@ -184,7 +214,10 @@ def _sample_distribution(self) -> List[float]: raise NotImplementedError def _should_use_startup_sampling(self) -> bool: - return self.random_start_tasks > 0 and self.completed_tasks < self.random_start_tasks + return ( + self.random_start_tasks > 0 + and self.completed_tasks < self.random_start_tasks + ) def _startup_sample(self) -> List: task_dist = [0.0 / self.num_tasks for _ in range(self.num_tasks)] @@ -209,6 +242,15 @@ def sample(self, k: int = 1) -> Union[List, Any]: task_idx = np.random.choice(list(range(n_tasks)), size=k, p=task_dist) return task_idx + def get_opponent(self, agent_id: int): + raise NotImplementedError + + def update_agent(self, agent): + raise NotImplementedError + + def update_winrate(self, opponent_id: int, opponent_reward: int): + raise NotImplementedError + def log_metrics(self, writer, step=None, log_full_dist=False): """Log the task distribution to the provided tensorboard writer. @@ -216,6 +258,7 @@ def log_metrics(self, writer, step=None, log_full_dist=False): """ try: import wandb + task_dist = self._sample_distribution() if len(task_dist) > 10 and not log_full_dist: warnings.warn("Only logging stats for 10 tasks.") diff --git a/syllabus/core/curriculum_sync_wrapper.py b/syllabus/core/curriculum_sync_wrapper.py index 12b3e93f..06fa61a1 100644 --- a/syllabus/core/curriculum_sync_wrapper.py +++ b/syllabus/core/curriculum_sync_wrapper.py @@ -8,12 +8,13 @@ from torch.multiprocessing import Lock, SimpleQueue from torch.utils.tensorboard import SummaryWriter -from syllabus.core import Curriculum, decorate_all_functions +from syllabus.core import Curriculum +from syllabus.core.utils import decorate_all_functions class CurriculumWrapper: - """Wrapper class for adding multiprocessing synchronization to a curriculum. - """ + """Wrapper class for adding multiprocessing synchronization to a curriculum.""" + def __init__(self, curriculum: Curriculum) -> None: self.curriculum = curriculum if hasattr(curriculum, "unwrapped") and curriculum.unwrapped is not None: @@ -47,6 +48,15 @@ def get_tasks(self, task_space=None): def sample(self, k=1): return self.curriculum.sample(k=k) + def get_opponent(self, agent_id: int): + return self.curriculum.get_opponent(agent_id) + + def update_agent(self, agent): + return self.curriculum.update_agent(agent) + + def update_winrate(self, opponent_id: int, opponent_reward: int): + return self.curriculum.update_winrate(opponent_id, opponent_reward) + def update_task_progress(self, task, progress): self.curriculum.update_task_progress(task, progress) @@ -164,7 +174,7 @@ def __init__( curriculum: Curriculum, task_queue: SimpleQueue, update_queue: SimpleQueue, - sequential_start: bool = True + sequential_start: bool = True, ): super().__init__(curriculum) self.task_queue = task_queue @@ -182,7 +192,9 @@ def start(self): """ Start the thread that reads the complete_queue and reads the task_queue. """ - self.update_thread = threading.Thread(name='update', target=self._update_queues, daemon=True) + self.update_thread = threading.Thread( + name="update", target=self._update_queues, daemon=True + ) self.should_update = True self.update_thread.start() @@ -267,6 +279,7 @@ def remote_call(func): Note that this causes functions to block, and should be only used for operations that do not require parallelization. """ + @wraps(func) def wrapper(self, *args, **kw): f_name = func.__name__ @@ -277,6 +290,7 @@ def wrapper(self, *args, **kw): if child_func == parent_func: curriculum_func = getattr(self.curriculum, f_name) return ray.get(curriculum_func.remote(*args, **kw)) + return wrapper @@ -287,7 +301,9 @@ def make_multiprocessing_curriculum(curriculum, **kwargs): task_queue = SimpleQueue() update_queue = SimpleQueue() - mp_curriculum = MultiProcessingCurriculumWrapper(curriculum, task_queue, update_queue, **kwargs) + mp_curriculum = MultiProcessingCurriculumWrapper( + curriculum, task_queue, update_queue, **kwargs + ) mp_curriculum.start() return mp_curriculum @@ -313,6 +329,7 @@ class RayCurriculumWrapper(CurriculumWrapper): for convenience. # TODO: Implement the Curriculum methods explicitly """ + def __init__(self, curriculum, actor_name="curriculum") -> None: super().__init__(curriculum) self.curriculum = RayWrapper.options(name=actor_name).remote(curriculum) @@ -325,7 +342,9 @@ def __init__(self, curriculum, actor_name="curriculum") -> None: def sample(self, k: int = 1): return ray.get(self.curriculum.sample.remote(k=k)) - def update_on_step_batch(self, step_results: List[Tuple[int, int, int, int]]) -> None: + def update_on_step_batch( + self, step_results: List[Tuple[int, int, int, int]] + ) -> None: ray.get(self.curriculum._on_step_batch.remote(step_results)) def add_task(self, task): diff --git a/syllabus/core/dual_curriculum_wrapper.py b/syllabus/core/dual_curriculum_wrapper.py new file mode 100644 index 00000000..d32b13d3 --- /dev/null +++ b/syllabus/core/dual_curriculum_wrapper.py @@ -0,0 +1,77 @@ +from typing import Tuple, TypeVar + +from gymnasium import spaces + +from syllabus.core.curriculum_base import Curriculum, TaskSpace + +AgentID = TypeVar("AgentID") +Agent = TypeVar("Agent") +EnvTask = TypeVar("EnvTask") +AgentTask = TypeVar("AgentTask") + + +class DualCurriculumWrapper(Curriculum): + """Curriculum wrapper containing both an agent and environment-based curriculum.""" + + def __init__( + self, + env_curriculum: Curriculum, + agent_curriculum: Curriculum, + batch_agent_tasks: bool = False, + batch_size: int = 32, + *args, + **kwargs, + ) -> None: + self.agent_curriculum = agent_curriculum + self.env_curriculum = env_curriculum + self.task_space = TaskSpace( + spaces.Dict( + { + "env_space": env_curriculum.task_space.gym_space, + "agent_space": agent_curriculum.task_space.gym_space, + } + ) + ) + self.batch_agent_tasks = batch_agent_tasks + self.batch_size = batch_size + self.batched_tasks = [] + self.agent_task = None + super().__init__(task_space=self.task_space, *args, **kwargs) + + def sample(self, k=1) -> Tuple[EnvTask, AgentTask]: + """Sets new tasks for the environment and agent curricula.""" + env_task = self.env_curriculum.sample(k=k) + if len(self.batched_tasks) < k: + self.batched_tasks = self.agent_curriculum.sample(k=1) * self.batch_size + agent_task = [self.batched_tasks.pop() for _ in range(k)] + return list(zip(env_task, agent_task)) + + def get_opponent(self, agent_task: AgentTask) -> Agent: + return self.agent_curriculum.get_opponent(agent_task) + + def update_agent(self, agent: Agent) -> Agent: + return self.agent_curriculum.update_agent(agent) + + def update_winrate(self, opponent_id: int, opponent_reward: int) -> None: + return self.agent_curriculum.update_winrate(opponent_id, opponent_reward) + + def update_on_step(self, task, obs, reward, term, trunc, info, env_id=None): + if self.env_curriculum.requires_step_updates: + self.env_curriculum.update_on_step( + task, obs, reward, term, trunc, info, env_id=env_id + ) + if self.agent_curriculum.requires_step_updates: + self.agent_curriculum.update_on_step( + task, obs, reward, term, trunc, info, env_id=env_id + ) + + def __getattr__(self, name): + """Delegate attribute lookup to the curricula if not found.""" + if hasattr(self.env_curriculum, name): + return getattr(self.env_curriculum, name) + elif hasattr(self.agent_curriculum, name): + return getattr(self.agent_curriculum, name) + else: + raise AttributeError( + f"'{self.__class__.__name__}' object has no attribute '{name}'" + ) diff --git a/syllabus/core/environment_sync_wrapper.py b/syllabus/core/environment_sync_wrapper.py index 4d968d6b..d1f46420 100644 --- a/syllabus/core/environment_sync_wrapper.py +++ b/syllabus/core/environment_sync_wrapper.py @@ -6,7 +6,7 @@ from gymnasium.utils.step_api_compatibility import step_api_compatibility from pettingzoo.utils.wrappers.base_parallel import BaseParallelWrapper -from syllabus.core import Curriculum, MultiProcessingComponents, MultiProcessingComponents +from syllabus.core import Curriculum, MultiProcessingComponents from syllabus.core.task_interface import PettingZooTaskWrapper, TaskEnv, TaskWrapper from syllabus.task_space import TaskSpace @@ -18,17 +18,23 @@ class MultiProcessingSyncWrapper(gym.Wrapper): with a QueueLearningProgressCurriculum running on the main process. """ - def __init__(self, - env, - components: MultiProcessingComponents, - update_on_step: bool = False, # TODO: Fine grained control over which step elements are used. Controlled by curriculum? - update_on_progress: bool = False, # TODO: Fine grained control over which step elements are used. Controlled by curriculum? - batch_size: int = 100, - buffer_size: int = 2, # Having an extra task in the buffer minimizes wait time at reset - task_space: TaskSpace = None, - global_task_completion: Callable[[Curriculum, np.ndarray, float, bool, Dict[str, Any]], bool] = None): + def __init__( + self, + env, + components: MultiProcessingComponents, + update_on_step: bool = False, # TODO: Fine grained control over which step elements are used. Controlled by curriculum? + update_on_progress: bool = False, # TODO: Fine grained control over which step elements are used. Controlled by curriculum? + batch_size: int = 100, + buffer_size: int = 2, # Having an extra task in the buffer minimizes wait time at reset + task_space: TaskSpace = None, + global_task_completion: Callable[ + [Curriculum, np.ndarray, float, bool, Dict[str, Any]], bool + ] = None, + ): # TODO: reimplement global task progress metrics - assert isinstance(task_space, TaskSpace), f"task_space must be a TaskSpace object. Got {type(task_space)} instead." + assert isinstance( + task_space, TaskSpace + ), f"task_space must be a TaskSpace object. Got {type(task_space)} instead." super().__init__(env) self.env = env self.components = components @@ -58,7 +64,9 @@ def __init__(self, self._task_progresses = [None] * self.batch_size # Request initial task - assert buffer_size > 0, "Buffer size must be greater than 0 to sample initial task for envs." + assert ( + buffer_size > 0 + ), "Buffer size must be greater than 0 to sample initial task for envs." for _ in range(buffer_size): update = { "update_type": "noop", @@ -72,7 +80,7 @@ def reset(self, *args, **kwargs): self.episode_length = 0 self.episode_return = 0 - message = self.components.get_task() # Blocks until a task is available + message = self.components.get_task() # Blocks until a task is available next_task = self.task_space.decode(message["next_task"]) self._latest_task = next_task @@ -86,7 +94,9 @@ def reset(self, *args, **kwargs): return obs, info def step(self, action): - obs, rew, term, trunc, info = step_api_compatibility(self.env.step(action), output_truncation_bool=True) + obs, rew, term, trunc, info = step_api_compatibility( + self.env.step(action), output_truncation_bool=True + ) self.episode_length += 1 self.episode_return += rew self.task_progress = info.get("task_completion", 0.0) @@ -113,15 +123,21 @@ def step(self, action): # Task progress task_update = { "update_type": "task_progress", - "metrics": ((self.task_space.encode(self.env.task), self.task_progress)), + "metrics": ( + (self.task_space.encode(self.env.task), self.task_progress) + ), "env_id": self.instance_id, "request_sample": False, } episode_update = { "update_type": "episode", - "metrics": (self.episode_return, self.episode_length, self.task_space.encode(self.env.task)), + "metrics": ( + self.episode_return, + self.episode_length, + self.task_space.encode(self.env.task), + ), "env_id": self.instance_id, - "request_sample": True + "request_sample": True, } self.components.put_update([task_update, episode_update]) @@ -132,27 +148,36 @@ def step(self, action): def _package_step_updates(self): step_batch = { "update_type": "step_batch", - "metrics": ([self._tasks[:self._batch_step], self._obs[:self._batch_step], self._rews[:self._batch_step], self._terms[:self._batch_step], self._truncs[:self._batch_step], self._infos[:self._batch_step]],), + "metrics": ( + [ + self._tasks[: self._batch_step], + self._obs[: self._batch_step], + self._rews[: self._batch_step], + self._terms[: self._batch_step], + self._truncs[: self._batch_step], + self._infos[: self._batch_step], + ], + ), "env_id": self.instance_id, - "request_sample": False + "request_sample": False, } update = [step_batch] if self.update_on_progress: task_batch = { "update_type": "task_progress_batch", - "metrics": (self._tasks[:self._batch_step], self._task_progresses[:self._batch_step],), + "metrics": ( + self._tasks[: self._batch_step], + self._task_progresses[: self._batch_step], + ), "env_id": self.instance_id, - "request_sample": False + "request_sample": False, } update.append(task_batch) return update def add_task(self, task): - update = { - "update_type": "add_task", - "metrics": task - } + update = {"update_type": "add_task", "metrics": task} self.update_queue.put(update) def get_task(self): @@ -247,8 +272,8 @@ def step(self, action): if "task_completion" in list(infos.values())[0]: self.task_progress = max([info["task_completion"] for info in infos.values()]) - is_finished = (len(self.env.agents) == 0) or all(terms.values()) + # Update curriculum with step info if self.update_on_step: agent_indices = [self.agent_map[agent] for agent in rews.keys()] @@ -306,10 +331,7 @@ def _package_step_updates(self): return update def add_task(self, task): - update = { - "update_type": "add_task", - "metrics": task - } + update = {"update_type": "add_task", "metrics": task} self.update_queue.put(update) def get_task(self): @@ -330,15 +352,24 @@ class RaySyncWrapper(gym.Wrapper): on parallel processes created using ray. Meant to be used with a RayLearningProgressCurriculum running on the main process. """ - def __init__(self, - env, - update_on_step: bool = True, - task_space: gym.Space = None, - global_task_completion: Callable[[Curriculum, np.ndarray, float, bool, Dict[str, Any]], bool] = None): - assert isinstance(env, TaskWrapper) or isinstance(env, TaskEnv) or isinstance(env, PettingZooTaskWrapper), "Env must implement the task API" + + def __init__( + self, + env, + update_on_step: bool = True, + task_space: gym.Space = None, + global_task_completion: Callable[ + [Curriculum, np.ndarray, float, bool, Dict[str, Any]], bool + ] = None, + ): + assert ( + isinstance(env, TaskWrapper) + or isinstance(env, TaskEnv) + or isinstance(env, PettingZooTaskWrapper) + ), "Env must implement the task API" super().__init__(env) self.env = env - self.update_on_step = update_on_step # Disable to improve performance + self.update_on_step = update_on_step # Disable to improve performance self.task_space = task_space self.curriculum = ray.get_actor("curriculum") self.task_completion = 0.0 @@ -352,7 +383,7 @@ def reset(self, *args, **kwargs): update = { "update_type": "task_progress", "metrics": (self.env.task, self.task_completion), - "request_sample": True + "request_sample": True, } self.curriculum.update.remote(update) self.task_completion = 0.0 @@ -369,7 +400,9 @@ def step(self, action): if "task_completion" in info: if self.global_task_completion is not None: # TODO: Hide rllib interface? - self.task_completion = self.global_task_completion(self.curriculum, obs, rew, term, trunc, info) + self.task_completion = self.global_task_completion( + self.curriculum, obs, rew, term, trunc, info + ) else: self.task_completion = info["task_completion"] @@ -380,7 +413,7 @@ def step(self, action): update = { "update_type": "step_batch", "metrics": (self.step_results,), - "request_sample": False + "request_sample": False, } self.curriculum.update.remote(update) self.step_results = [] @@ -415,15 +448,24 @@ class PettingZooRaySyncWrapper(BaseParallelWrapper): on parallel processes created using ray. Meant to be used with a RayLearningProgressCurriculum running on the main process. """ - def __init__(self, - env, - update_on_step: bool = True, - task_space: gym.Space = None, - global_task_completion: Callable[[Curriculum, np.ndarray, float, bool, Dict[str, Any]], bool] = None): - assert isinstance(env, TaskWrapper) or isinstance(env, TaskEnv) or isinstance(env, PettingZooTaskWrapper), "Env must implement the task API" + + def __init__( + self, + env, + update_on_step: bool = True, + task_space: gym.Space = None, + global_task_completion: Callable[ + [Curriculum, np.ndarray, float, bool, Dict[str, Any]], bool + ] = None, + ): + assert ( + isinstance(env, TaskWrapper) + or isinstance(env, TaskEnv) + or isinstance(env, PettingZooTaskWrapper) + ), "Env must implement the task API" super().__init__(env) self.env = env - self.update_on_step = update_on_step # Disable to improve performance + self.update_on_step = update_on_step # Disable to improve performance self.task_space = task_space self.curriculum = ray.get_actor("curriculum") self.task_completion = 0.0 @@ -437,7 +479,7 @@ def reset(self, *args, **kwargs): update = { "update_type": "task_progress", "metrics": (self.env.task, self.task_completion), - "request_sample": True + "request_sample": True, } self.curriculum.update.remote(update) self.task_completion = 0.0 @@ -454,7 +496,9 @@ def step(self, action): if "task_completion" in info: if self.global_task_completion is not None: # TODO: Hide rllib interface? - self.task_completion = self.global_task_completion(self.curriculum, obs, rew, term, trunc, info) + self.task_completion = self.global_task_completion( + self.curriculum, obs, rew, term, trunc, info + ) else: self.task_completion = info["task_completion"] @@ -465,7 +509,7 @@ def step(self, action): update = { "update_type": "step_batch", "metrics": (self.step_results,), - "request_sample": False + "request_sample": False, } self.curriculum.update.remote(update) self.step_results = [] diff --git a/syllabus/core/multiagent_curriculum_wrappers.py b/syllabus/core/multiagent_curriculum_wrappers.py index 411e0b1a..f59ed1b3 100644 --- a/syllabus/core/multiagent_curriculum_wrappers.py +++ b/syllabus/core/multiagent_curriculum_wrappers.py @@ -9,27 +9,48 @@ def __init__(self, curriculum, possible_agents, *args, **kwargs): def update_task_progress(self, task, progress, env_id=None): for i in range(self.num_agents): - self.curriculum.update_task_progress(task, progress, env_id=(env_id * self.num_agents) + i) + self.curriculum.update_task_progress( + task, progress, env_id=(env_id * self.num_agents) + i + ) - def update_on_step(self, task, obs, rew, term, trunc, info, env_id: int = None) -> None: + def update_on_step( + self, task, obs, rew, term, trunc, info, env_id: int = None + ) -> None: """ Update the curriculum with the current step results from the environment. """ for i, agent in enumerate(obs.keys()): agent_index = self.possible_agents.index(agent) - self.curriculum.update_on_step(task, obs[agent], rew[i], term[i], trunc[i], info[agent], env_id=(env_id * self.num_agents) + agent_index) + self.curriculum.update_on_step( + task, + obs[agent], + rew[i], + term[i], + trunc[i], + info[agent], + env_id=(env_id * self.num_agents) + agent_index, + ) def update_on_step_batch(self, step_results, env_id: int = None) -> None: tasks, obs, rews, terms, truncs, infos = step_results for i in range(len(obs)): - self.update_on_step(tasks[i], obs[i], rews[i], terms[i], truncs[i], infos[i], env_id=env_id) + self.update_on_step( + tasks[i], obs[i], rews[i], terms[i], truncs[i], infos[i], env_id=env_id + ) - def update_on_episode(self, episode_returns, episode_length, episode_task, env_id: int = None) -> None: + def update_on_episode( + self, episode_returns, episode_length, episode_task, env_id: int = None + ) -> None: """ Update the curriculum with episode results from the environment. """ for i, agent in enumerate(episode_returns.keys()): - self.curriculum.update_on_episode(episode_returns[agent], episode_length, episode_task, env_id=(env_id * self.num_agents) + i) + self.curriculum.update_on_episode( + episode_returns[agent], + episode_length, + episode_task, + env_id=(env_id * self.num_agents) + i, + ) def update_batch(self, update_data): for update in update_data: diff --git a/syllabus/curricula/__init__.py b/syllabus/curricula/__init__.py index d3881dd6..369b9abe 100644 --- a/syllabus/curricula/__init__.py +++ b/syllabus/curricula/__init__.py @@ -1,3 +1,4 @@ +# flake8: noqa: F401 import sys from .domain_randomization import DomainRandomization, BatchedDomainRandomization, SyncedBatchedDomainRandomization @@ -6,6 +7,7 @@ from .plr.central_plr_wrapper import CentralizedPrioritizedLevelReplay from .plr.plr_wrapper import PrioritizedLevelReplay from .plr.task_sampler import TaskSampler +from .selfplay import FictitiousSelfPlay, PrioritizedFictitiousSelfPlay, SelfPlay from .sequential import SequentialCurriculum from .simple_box import SimpleBoxCurriculum from .annealing_box import AnnealingBoxCurriculum diff --git a/syllabus/curricula/domain_randomization.py b/syllabus/curricula/domain_randomization.py index 40493b47..30e2eff4 100644 --- a/syllabus/curricula/domain_randomization.py +++ b/syllabus/curricula/domain_randomization.py @@ -5,12 +5,16 @@ class DomainRandomization(Curriculum): - """A simple but strong baseline for curriculum learning that uniformly samples a task from the task space. - """ + """A simple but strong baseline for curriculum learning that uniformly samples a task from the task space.""" + REQUIRES_STEP_UPDATES = False REQUIRES_EPISODE_UPDATES = False REQUIRES_CENTRAL_UPDATES = False + @property + def name(self): + return "DR" + def _sample_distribution(self) -> List[float]: """ Returns a sample distribution over the task space. diff --git a/syllabus/curricula/plr/central_plr_wrapper.py b/syllabus/curricula/plr/central_plr_wrapper.py index 7f69ea85..6211aae5 100644 --- a/syllabus/curricula/plr/central_plr_wrapper.py +++ b/syllabus/curricula/plr/central_plr_wrapper.py @@ -1,5 +1,5 @@ import warnings -from typing import Any, Dict, List, Tuple, Union +from typing import Any, Dict, List, Union import gymnasium as gym import torch @@ -47,9 +47,13 @@ def to(self, device): else: self.action_log_dist = self.action_log_dist.to(device) - def insert(self, masks, action_log_dist=None, value_preds=None, rewards=None, tasks=None): + def insert( + self, masks, action_log_dist=None, value_preds=None, rewards=None, tasks=None + ): if self._requires_value_buffers: - assert (value_preds is not None and rewards is not None), "Selected strategy requires value_preds and rewards" + assert ( + value_preds is not None and rewards is not None + ), "Selected strategy requires value_preds and rewards" if len(rewards.shape) == 3: rewards = rewards.squeeze(2) self.value_preds[self.step].copy_(torch.as_tensor(value_preds)) @@ -66,7 +70,9 @@ def after_update(self): self.masks[0].copy_(self.masks[-1]) def compute_returns(self, next_value, gamma, gae_lambda): - assert self._requires_value_buffers, "Selected strategy does not use compute_rewards." + assert ( + self._requires_value_buffers + ), "Selected strategy does not use compute_rewards." self.value_preds[-1] = next_value gae = 0 for step in reversed(range(self.rewards.size(0))): @@ -80,7 +86,7 @@ def compute_returns(self, next_value, gamma, gae_lambda): class CentralizedPrioritizedLevelReplay(Curriculum): - """ Prioritized Level Replay (PLR) Curriculum. + """Prioritized Level Replay (PLR) Curriculum. Args: task_space (TaskSpace): The task space to use for the curriculum. @@ -95,6 +101,7 @@ class CentralizedPrioritizedLevelReplay(Curriculum): suppress_usage_warnings (bool): Whether to suppress warnings about improper usage. **curriculum_kwargs: Keyword arguments to pass to the curriculum. """ + REQUIRES_STEP_UPDATES = False REQUIRES_EPISODE_UPDATES = False REQUIRES_CENTRAL_UPDATES = True @@ -113,17 +120,25 @@ def __init__( suppress_usage_warnings=False, **curriculum_kwargs, ): + self.name = "PLR" # Preprocess curriculum intialization args if task_sampler_kwargs_dict is None: task_sampler_kwargs_dict = {} self._strategy = task_sampler_kwargs_dict.get("strategy", None) - if not isinstance(task_space.gym_space, Discrete) and not isinstance(task_space.gym_space, MultiDiscrete): + if not isinstance(task_space.gym_space, Discrete) and not isinstance( + task_space.gym_space, MultiDiscrete + ): raise ValueError( f"Task space must be discrete or multi-discrete, got {task_space.gym_space}." ) - if "num_actors" in task_sampler_kwargs_dict and task_sampler_kwargs_dict['num_actors'] != num_processes: - warnings.warn(f"Overwriting 'num_actors' {task_sampler_kwargs_dict['num_actors']} in task sampler kwargs with PLR num_processes {num_processes}.") + if ( + "num_actors" in task_sampler_kwargs_dict + and task_sampler_kwargs_dict["num_actors"] != num_processes + ): + warnings.warn( + f"Overwriting 'num_actors' {task_sampler_kwargs_dict['num_actors']} in task sampler kwargs with PLR num_processes {num_processes}." + ) task_sampler_kwargs_dict["num_actors"] = num_processes super().__init__(task_space, *curriculum_args, **curriculum_kwargs) @@ -133,7 +148,9 @@ def __init__( self._gae_lambda = gae_lambda self._supress_usage_warnings = suppress_usage_warnings self._task2index = {task: i for i, task in enumerate(self.tasks)} - self._task_sampler = TaskSampler(self.tasks, action_space=action_space, **task_sampler_kwargs_dict) + self._task_sampler = TaskSampler( + self.tasks, action_space=action_space, **task_sampler_kwargs_dict + ) self._rollouts = RolloutStorage( self._num_steps, self._num_processes, @@ -187,7 +204,9 @@ def update_on_demand(self, metrics: Dict): Update the curriculum with arbitrary inputs. """ self.num_updates += 1 - masks, tasks, value, rew, action_log_dist, next_value = self._validate_metrics(metrics) + masks, tasks, value, rew, action_log_dist, next_value = self._validate_metrics( + metrics + ) # Update rollouts self._rollouts.insert( @@ -201,7 +220,9 @@ def update_on_demand(self, metrics: Dict): # Update task sampler if self._rollouts.step == 0: if self._task_sampler.requires_value_buffers: - self._rollouts.compute_returns(next_value, self._gamma, self._gae_lambda) + self._rollouts.compute_returns( + next_value, self._gamma, self._gae_lambda + ) self._task_sampler.update_with_rollouts(self._rollouts) self._rollouts.after_update() self._task_sampler.after_update() @@ -220,7 +241,9 @@ def sample(self, k: int = 1) -> Union[List, Any]: return [self._task_sampler.sample() for _ in range(k)] def _enumerate_tasks(self, space): - assert isinstance(space, Discrete) or isinstance(space, MultiDiscrete), f"Unsupported task space {space}: Expected Discrete or MultiDiscrete" + assert isinstance(space, Discrete) or isinstance( + space, MultiDiscrete + ), f"Unsupported task space {space}: Expected Discrete or MultiDiscrete" if isinstance(space, Discrete): return list(range(space.n)) else: @@ -232,8 +255,18 @@ def log_metrics(self, writer, step=None): """ super().log_metrics(writer, step) metrics = self._task_sampler.metrics() - writer.add_scalar("curriculum/proportion_seen", metrics["proportion_seen"], step) + writer.add_scalar( + "curriculum/proportion_seen", metrics["proportion_seen"], step + ) writer.add_scalar("curriculum/score", metrics["score"], step) for task in list(self.task_space.tasks)[:10]: - writer.add_scalar(f"curriculum/task_{task - 1}_score", metrics["task_scores"][task - 1], step) - writer.add_scalar(f"curriculum/task_{task - 1}_staleness", metrics["task_staleness"][task - 1], step) + writer.add_scalar( + f"curriculum/task_{task - 1}_score", + metrics["task_scores"][task - 1], + step, + ) + writer.add_scalar( + f"curriculum/task_{task - 1}_staleness", + metrics["task_staleness"][task - 1], + step, + ) diff --git a/syllabus/curricula/selfplay.py b/syllabus/curricula/selfplay.py new file mode 100644 index 00000000..2019cc18 --- /dev/null +++ b/syllabus/curricula/selfplay.py @@ -0,0 +1,255 @@ +import os +import time +from copy import deepcopy +from typing import TypeVar + +import joblib +import numpy as np +from gymnasium import spaces +from scipy.special import softmax + +from syllabus.core import Curriculum # noqa: E402 +from syllabus.task_space import TaskSpace # noqa: E402 + +AgentType = TypeVar("AgentType") + + +class SelfPlay(Curriculum): + REQUIRES_STEP_UPDATES = False + REQUIRES_EPISODE_UPDATES = False + REQUIRES_CENTRAL_UPDATES = False + + def __init__( + self, + agent: AgentType, + device: str, + storage_path=None, # unused + max_agents=None, # unused + seed: int = 0, + ): + self.name = "SP" + self.device = device + self.agent = deepcopy(agent).to(self.device) + self.task_space = TaskSpace( + spaces.Discrete(1) + ) # SelfPlay can only return agent_id = 0 + self.history = { + "winrate": 0, + "n_games": 0, + } + + def update_agent(self, agent: AgentType) -> AgentType: + self.agent = deepcopy(agent).to(self.device) + + def get_opponent(self, agent_id: int) -> AgentType: + if agent_id is None: + agent_id = 0 + assert agent_id == 0, ( + f"Self play only tracks the current agent." + f"Expected agent id 0, got {agent_id}" + ) + return self.agent + + def sample(self, k=1): + return [0 for _ in range(k)] + + def update_winrate(self, opponent_id: int, opponent_reward: int) -> None: + """ + Uses an incremental mean to update the opponent's winrate. + """ + opponent_reward = opponent_reward > 0 # converts the reward to 0 or 1 + self.history["n_games"] += 1 + old_winrate = self.history["winrate"] + n = self.history["n_games"] + + self.history["winrate"] = old_winrate + (opponent_reward - old_winrate) / n + + +class FictitiousSelfPlay(Curriculum): + REQUIRES_STEP_UPDATES = False + REQUIRES_EPISODE_UPDATES = False + REQUIRES_CENTRAL_UPDATES = False + + def __init__( + self, + agent: AgentType, + device: str, + storage_path: str, + max_agents: int, + seed: int = 0, + max_loaded_agents: int = 1, + ): + self.name = "FSP" + self.uid = int(time.time()) + self.device = device + self.storage_path = storage_path + self.seed = seed + + if not os.path.exists(self.storage_path): + os.makedirs(self.storage_path, exist_ok=True) + + self.current_agent_index = 0 + self.max_agents = max_agents + self.task_space = TaskSpace(spaces.Discrete(self.max_agents)) + self.update_agent(agent) # creates the initial opponent + self.history = { + i: { + "winrate": 0, + "n_games": 0, + } + for i in range(self.max_agents) + } + self.loaded_agents = {i: None for i in range(self.max_agents)} + self.n_loaded_agents = 0 + self.max_loaded_agents = max_loaded_agents + + def update_agent(self, agent): + """ + Saves the current agent instance to a pickle file. + When the `max_agents` limit is met, older agent checkpoints are overwritten. + """ + agent = agent.to("cpu") + joblib.dump( + agent, + filename=( + f"{self.storage_path}/{self.name}_{self.seed}_agent_checkpoint_" + f"{self.current_agent_index % self.max_agents}.pkl" + ), + ) + agent = agent.to(self.device) + if self.current_agent_index < self.max_agents: + self.current_agent_index += 1 + + def update_winrate(self, opponent_id: int, opponent_reward: int) -> None: + """ + Uses an incremental mean to update the opponent's winrate i.e. priority. + This implies that sampling according to the winrates returns the most + challenging opponents. + """ + opponent_reward = opponent_reward > 0 # converts the reward to 0 or 1 + self.history[opponent_id]["n_games"] += 1 + old_winrate = self.history[opponent_id]["winrate"] + n = self.history[opponent_id]["n_games"] + + self.history[opponent_id]["winrate"] = ( + old_winrate + (opponent_reward - old_winrate) / n + ) + + def get_opponent(self, agent_id: int) -> AgentType: + """Loads an agent from the buffer of saved agents.""" + if self.loaded_agents[agent_id] is None: + if self.n_loaded_agents >= self.max_loaded_agents: + pass + print( + "get agent", + agent_id, + f"{self.storage_path}/{self.name}_{self.seed}_agent_checkpoint_{agent_id}.pkl", + ) + self.loaded_agents[agent_id] = joblib.load( + f"{self.storage_path}/{self.name}_{self.seed}_agent_checkpoint_{agent_id}.pkl" + ).to(self.device) + + return self.loaded_agents[agent_id] + + def sample(self, k=1): + return list(np.random.randint(self.current_agent_index, size=k)) + + +class PrioritizedFictitiousSelfPlay(Curriculum): + REQUIRES_STEP_UPDATES = False + REQUIRES_EPISODE_UPDATES = False + REQUIRES_CENTRAL_UPDATES = False + + def __init__( + self, + agent: AgentType, + device: str, + storage_path: str, + max_agents: int, + seed: int = 0, + max_loaded_agents: int = 1, + ): + self.name = "PFSP" + self.uid = int(time.time()) + self.device = device + self.storage_path = storage_path + self.seed = seed + if not os.path.exists(self.storage_path): + os.makedirs(self.storage_path, exist_ok=True) + + self.current_agent_index = 0 + self.max_agents = max_agents + self.task_space = TaskSpace(spaces.Discrete(self.max_agents)) + self.update_agent(agent) # creates the initial opponent + self.history = { + i: { + "winrate": 0, + "n_games": 0, + } + for i in range(self.max_agents) + } + self.loaded_agents = {i: None for i in range(self.max_agents)} + self.n_loaded_agents = 0 + self.max_loaded_agents = max_loaded_agents + + def update_agent(self, agent) -> None: + """ + Saves the current agent instance to a pickle file and update + its priority. + """ + agent = agent.to("cpu") + joblib.dump( + agent, + filename=( + f"{self.storage_path}/{self.name}_{self.seed}_agent_checkpoint_" + f"{self.current_agent_index % self.max_agents}.pkl" + ), + ) + agent = agent.to(self.device) + if self.current_agent_index < self.max_agents: + self.current_agent_index += 1 + + def update_winrate(self, opponent_id: int, opponent_reward: int) -> None: + """ + Uses an incremental mean to update the opponent's winrate i.e. priority. + This implies that sampling according to the winrates returns the most + challenging opponents. + """ + opponent_reward = opponent_reward > 0 # converts the reward to 0 or 1 + self.history[opponent_id]["n_games"] += 1 + old_winrate = self.history[opponent_id]["winrate"] + n = self.history[opponent_id]["n_games"] + + self.history[opponent_id]["winrate"] = ( + old_winrate + (opponent_reward - old_winrate) / n + ) + + def get_opponent(self, agent_id: int) -> AgentType: + """ + Samples an agent id from the softmax distribution induced by winrates + then loads the selected agent from the buffer of saved agents. + """ + if self.loaded_agents[agent_id] is None: + if self.n_loaded_agents >= self.max_loaded_agents: + pass + print( + "get agent", + agent_id, + f"{self.storage_path}/{self.name}_{self.seed}_agent_checkpoint_{agent_id}.pkl", + ) + self.loaded_agents[agent_id] = joblib.load( + f"{self.storage_path}/{self.name}_{self.seed}_agent_checkpoint_{agent_id}.pkl" + ).to(self.device) + + return self.loaded_agents[agent_id] + + def sample(self, k=1): + logits = [ + self.history[agent_id]["winrate"] + for agent_id in range(self.current_agent_index) + ] + return list(np.random.choice( + np.arange(self.current_agent_index), + p=softmax(logits), + size=k, + )) diff --git a/syllabus/examples/experimental/lasertag_main.py b/syllabus/examples/experimental/lasertag_main.py new file mode 100644 index 00000000..de233a6e --- /dev/null +++ b/syllabus/examples/experimental/lasertag_main.py @@ -0,0 +1,655 @@ +import argparse +import os +import sys +import time +from typing import Dict, Tuple, TypeVar + +import joblib +import numpy as np +import pandas as pd +import plotly +import plotly.express as px +import plotly.graph_objects as go +import torch +import torch.nn as nn +import torch.optim as optim +import wandb +from gymnasium import spaces +from plotly.subplots import make_subplots +from torch.distributions.categorical import Categorical +from torch.utils.tensorboard import SummaryWriter +from tqdm.auto import tqdm + +sys.path.append("../../..") +from lasertag import LasertagAdversarial # noqa: E402 +from syllabus.core import ( # noqa: E402 + DualCurriculumWrapper, + TaskWrapper, + make_multiprocessing_curriculum, +) + +# noqa: E402 +from syllabus.curricula import ( # noqa: E402 + CentralizedPrioritizedLevelReplay, + DomainRandomization, + FictitiousSelfPlay, + PrioritizedFictitiousSelfPlay, + SelfPlay, +) +from syllabus.task_space import TaskSpace # noqa: E402 + +ActionType = TypeVar("ActionType") +AgentID = TypeVar("AgentID") +AgentType = TypeVar("AgentType") +EnvTask = TypeVar("EnvTask") +AgentTask = TypeVar("AgentTask") +ObsType = TypeVar("ObsType") + + +def parse_args(): + parser = argparse.ArgumentParser() + parser.add_argument("--track", type=bool, default=False) + parser.add_argument("--total-updates", type=int, default=40000) + parser.add_argument("--rollout_length", type=int, default=256) + parser.add_argument( + "--batch-size", type=int, default=32 + ) # TODO: determine correct value + + # PPO args + parser.add_argument("--ent-coef", type=float, default=0.0) + parser.add_argument("--vf-coef", type=float, default=0.5) + parser.add_argument("--clip-coef", type=float, default=0.2) + parser.add_argument("--learning-rate", type=float, default=1e-4) + parser.add_argument("--epsilon", type=float, default=1e-5) + parser.add_argument("--gamma", type=float, default=0.995) + parser.add_argument("--gae-lambda", type=float, default=0.95) + parser.add_argument("--epochs", type=int, default=5) + + # curriculum args + parser.add_argument( + "--agent-curriculum", type=str, default="SP", choices=["SP", "FSP", "PFSP"] + ) + parser.add_argument( + "--env-curriculum", type=str, default="DR", choices=["DR", "PLR"] + ) + parser.add_argument("--agent-update-frequency", type=int, default=8000) + # number of opponents in FSP and PFSP + parser.add_argument("--max-agents", type=int, default=10) + parser.add_argument("--save-agent-checkpoints", type=bool, default=False) + parser.add_argument( + "--checkpoint-frequency", type=int, default=4000 + ) # agent checkpoints every N steps + parser.add_argument("--n-env-tasks", type=int, default=4000) + parser.add_argument("--seed", type=int, default=0) + parser.add_argument( + "--exp-name", + type=str, + default="lasertag_", + help="the name of this experiment", + ) + parser.add_argument( + "--wandb-project-name", + type=str, + default="syllabus", + help="the wandb's project name", + ) + parser.add_argument( + "--wandb-entity", + type=str, + default="rpegoud", + help="the entity (team) of wandb's project", + ) + parser.add_argument( + "--logging-dir", + type=str, + default=".", + help="the base directory for logging and wandb storage.", + ) + + args = parser.parse_args() + return args + + +def batchify(x, device): + """Converts PZ style returns to batch of torch arrays.""" + # convert to list of np arrays + x = np.stack([x[a] for a in x], axis=0) + # convert to torch + x = torch.tensor(x).to(device) + + return x + + +def unbatchify(x, possible_agents: np.ndarray): + """Converts np array to PZ style arguments.""" + x = x.cpu().numpy() + x = {agent: x[idx] for idx, agent in enumerate(possible_agents)} + + return x + + +class LasertagParallelWrapper(TaskWrapper): + """ + Wrapper ensuring compatibility with the PettingZoo Parallel API. + + Lasertag Environment: + * Action shape: `n_agents` * `Discrete(5)` + * Observation shape: Dict('image': Box(0, 255, (`n_agents`, 3, 5, 5), uint8)) + """ + + def __init__(self, n_agents, *args, **kwargs): + super().__init__(*args, **kwargs) + self.n_agents = n_agents + self.task = None + self.episode_return = 0 + self.possible_agents = [f"agent_{i}" for i in range(self.n_agents)] + self.n_steps = 0 + + def __getattr__(self, name): + """ + Delegate attribute lookup to the wrapped environment if the attribute + is not found in the LasertagParallelWrapper instance. + """ + return getattr(self.env, name) + + def _np_array_to_pz_dict(self, array: np.ndarray) -> Dict[str, np.ndarray]: + """ + Returns a dictionary containing individual observations for each agent. + Assumes that the batch dimension represents individual agents. + """ + out = {} + for idx, value in enumerate(array): + out[self.possible_agents[idx]] = value + return out + + def _singleton_to_pz_dict(self, value: bool) -> Dict[str, bool]: + """ + Broadcasts the `done` and `trunc` flags to dictionaries keyed by agent id. + """ + return {str(agent_index): value for agent_index in range(self.n_agents)} + + def reset( + self, env_task: int + ) -> Tuple[Dict[AgentID, ObsType], Dict[AgentID, dict]]: + """ + Resets the environment and returns a dictionary of observations + keyed by agent ID. + """ + self.env.seed(env_task) + obs = self.env.reset_random() # random level generation + pz_obs = self._np_array_to_pz_dict(obs["image"]) + + return pz_obs + + def step( + self, action: Dict[AgentID, ActionType], device: str, agent_task: int + ) -> Tuple[ + Dict[AgentID, ObsType], + Dict[AgentID, float], + Dict[AgentID, bool], + Dict[AgentID, bool], + Dict[AgentID, dict], + ]: + """ + Takes inputs in the PettingZoo (PZ) Parallel API format, performs a step and + returns outputs in PZ format. + """ + action = batchify(action, device) + obs, rew, done, info = self.env.step(action) + obs = obs["image"] + trunc = False # there is no `truncated` flag in this environment + self.task_completion = self._task_completion(obs, rew, done, trunc, info) + # convert outputs back to PZ format + obs, rew = map(self._np_array_to_pz_dict, [obs, rew]) + done, trunc, info = map( + self._singleton_to_pz_dict, [done, trunc, self.task_completion] + ) + info["agent_id"] = agent_task + self.n_steps += 1 + + return self.observation(obs), rew, done, trunc, info + + +class Agent(nn.Module): + def __init__(self, num_actions): + super().__init__() + + self.network = nn.Sequential( + self._layer_init(nn.Linear(3 * 5 * 5, 512)), + nn.ReLU(), + ) + self.actor = self._layer_init(nn.Linear(512, num_actions), std=0.01) + self.critic = self._layer_init(nn.Linear(512, 1)) + + def _layer_init(self, layer, std=np.sqrt(2), bias_const=0.0): + torch.nn.init.orthogonal_(layer.weight, std) + torch.nn.init.constant_(layer.bias, bias_const) + return layer + + def get_value(self, x, flatten_start_dim=1): + x = torch.flatten(x, start_dim=flatten_start_dim) + return self.critic(self.network(x / 255.0)) + + def get_action_and_value(self, x, action=None, flatten_start_dim=1): + x = torch.flatten(x, start_dim=flatten_start_dim) + hidden = self.network(x / 255.0) + logits = self.actor(hidden) + probs = Categorical(logits=logits) + if action is None: + action = probs.sample() + return action, probs.log_prob(action), probs.entropy(), self.critic(hidden) + + +agent_curriculums = { + "SP": SelfPlay, + "FSP": FictitiousSelfPlay, + "PFSP": PrioritizedFictitiousSelfPlay, +} +env_curriculums = { + "DR": DomainRandomization, + "PLR": CentralizedPrioritizedLevelReplay, +} + +if __name__ == "__main__": + args = parse_args() + exp_name = f"{args.exp_name}_{args.env_curriculum}_{args.agent_curriculum}" + run_name = f"lasertag__{exp_name}__seed_{args.seed}__{int(time.time())}" + + if args.track: + wandb.init( + project=args.wandb_project_name, + entity=args.wandb_entity, + sync_tensorboard=True, + config=vars(args), + name=run_name, + monitor_gym=True, + save_code=True, + dir=args.logging_dir, + ) + wandb.run.log_code(os.path.join(args.logging_dir)) + + hyperparameters = vars(args) + html_table = "" + for key, value in hyperparameters.items(): + html_table += f"" + wandb.log({"hyperparameters": wandb.Html(html_table)}) + + writer = SummaryWriter( + os.path.join(args.logging_dir, f"{args.logging_dir}/runs/{run_name}") + ) + writer.add_text( + "hyperparameters", + "|param|value|\n|-|-|\n%s" + % ("\n".join([f"|{key}|{value}|" for key, value in vars(args).items()])), + ) + + if ( + not os.path.exists(f"{args.logging_dir}/{exp_name}_checkpoints") + and args.save_agent_checkpoints + ): + os.makedirs(f"{args.logging_dir}/{exp_name}_checkpoints", exist_ok=True) + + np.random.seed(args.seed) + + """ALGO PARAMS""" + device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + + stack_size = 3 + frame_size = (5, 5) + n_agents = 2 + num_actions = 5 + + agent_curriculum_settings = { + "device": device, + "storage_path": f"{args.agent_curriculum}_agents", + "max_agents": args.max_agents, + "seed": args.seed, + } + + env_task_space = TaskSpace(spaces.Discrete(args.n_env_tasks)) + env_curriculum_settings = { + "DR": {"task_space": env_task_space}, + "PLR": { + "task_space": env_task_space, + "num_steps": args.rollout_length, + "num_processes": 1, # TODO: modify if using vecenvs + "gamma": args.gamma, + "gae_lambda": args.gae_lambda, + "task_sampler_kwargs_dict": {"strategy": "value_l1"}, + }, + } + + """ LEARNER SETUP """ + agent = Agent(num_actions=num_actions).to(device) + optimizer = optim.Adam(agent.parameters(), lr=args.learning_rate, eps=args.epsilon) + + """ ENV SETUP """ + env = LasertagAdversarial(record_video=False) # 2 agents by default + env = LasertagParallelWrapper(env=env, n_agents=n_agents) + agent_curriculum = agent_curriculums[args.agent_curriculum]( + agent=agent, **agent_curriculum_settings + ) + + env_curriculum = env_curriculums[args.env_curriculum]( + **env_curriculum_settings[args.env_curriculum] + ) + + curriculum = DualCurriculumWrapper( + env=env, + agent_curriculum=agent_curriculum, + env_curriculum=env_curriculum, + ) + mp_curriculum = make_multiprocessing_curriculum(curriculum) + + """ ALGO LOGIC: EPISODE STORAGE""" + total_episodic_return = 0 + rb_obs = torch.zeros((args.rollout_length, n_agents, stack_size, *frame_size)).to( + device + ) + rb_actions = torch.zeros((args.rollout_length, n_agents)).to(device) + rb_logprobs = torch.zeros((args.rollout_length, n_agents)).to(device) + rb_rewards = torch.zeros((args.rollout_length, n_agents)).to(device) + rb_terms = torch.zeros((args.rollout_length, n_agents)).to(device) + rb_values = torch.zeros((args.rollout_length, n_agents)).to(device) + + agent_tasks, env_tasks, rewards_history = [], [], [] + agent_c_rew, opp_c_rew = 0, 0 + episode, n_learner_wins = 0, 0 + n_updates = 0 + info = {} + + """ TRAINING LOGIC """ + with tqdm(total=args.total_updates) as pbar: + while n_updates < args.total_updates: + with torch.no_grad(): + env_task, agent_task = mp_curriculum.sample() + + env_tasks.append(env_task) + agent_tasks.append(agent_task) + + next_obs = env.reset(env_task) + total_episodic_return = 0 + + for step in range(0, args.rollout_length): + joint_obs = batchify(next_obs, device).squeeze() + agent_obs, opponent_obs = joint_obs + + actions, logprobs, _, values = agent.get_action_and_value( + agent_obs, flatten_start_dim=0 + ) + + opponent = mp_curriculum.get_opponent(info.get("agent_id", 0)).to( + device + ) + opponent_action, *_ = opponent.get_action_and_value( + opponent_obs, flatten_start_dim=0 + ) + + joint_actions = torch.tensor((actions, opponent_action)) + next_obs, rewards, dones, truncs, info = env.step( + unbatchify(joint_actions, env.possible_agents), + device, + agent_task, + ) + + opp_reward = rewards["agent_1"] + if opp_reward != 0: + mp_curriculum.update_winrate(info["agent_id"], opp_reward) + if opp_reward == -1: + n_learner_wins += 1 + + rb_obs[step] = batchify(next_obs, device) + rb_rewards[step] = batchify(rewards, device) + rb_terms[step] = batchify(dones, device) + rb_actions[step] = joint_actions + rb_logprobs[step] = logprobs + rb_values[step] = values.flatten() + + total_episodic_return += rb_rewards[step].cpu().numpy() + + agent_c_rew += rewards["agent_0"] + opp_c_rew += rewards["agent_1"] + grid_size = env.level[3]["grid_size_selected"] + walls_percentage = env.level[3]["clutter_rate_selected"] + + if any([dones[a] for a in dones]) or any( + [truncs[a] for a in truncs] + ): + episode += 1 + rewards_history.append(rewards) + env_task, agent_task = mp_curriculum.sample() + env_tasks.append(env_task) + agent_tasks.append(agent_task) + + writer.add_scalar("charts/grid_size", grid_size, episode) + writer.add_scalar( + "charts/walls_percentage", walls_percentage, episode + ) + + next_obs = env.reset(env_task) + + # store learner checkpoints + if ( + args.save_agent_checkpoints + and n_updates % args.checkpoint_frequency == 0 + ): + print(f"saving checkpoint --{n_updates}") + checkpoint_path = ( + f"{args.logging_dir}/{exp_name}_checkpoints/" + f"{mp_curriculum.curriculum.env_curriculum.name}_" + f"{mp_curriculum.curriculum.agent_curriculum.name}_{n_updates}" + f"_seed_{args.seed}.pkl" + ) + # --- local checkpoint --- + joblib.dump( + agent, + filename=checkpoint_path, + ) + + # --- wandb checkpoint --- + if args.track: + agent_artifact = wandb.Artifact("model", type="model") + agent_artifact.add_file(checkpoint_path) + wandb.log_artifact(agent_artifact) + + if args.env_curriculum == "PLR": + with torch.no_grad(): + next_value = agent.get_value( + torch.tensor(next_obs["agent_0"]).to(device), + flatten_start_dim=0, + ) + update = { + "update_type": "on_demand", + "metrics": { + "value": values, + "next_value": next_value, + "rew": rewards[ + "agent_0" + ], # TODO: is this the expected use? + "dones": dones, + "tasks": env_task, + }, + } + + # gae + with torch.no_grad(): + next_value = agent.get_value( + torch.tensor(next_obs["agent_0"]).to(device), flatten_start_dim=0 + ) + rb_advantages = torch.zeros_like(rb_rewards).to(device) + last_gae_lam = 0 + for t in reversed(range(args.rollout_length - 1)): + if t == args.rollout_length - 1: + next_non_terminal = 1.0 - rb_terms[t + 1] + next_values = next_value + else: + next_non_terminal = 1.0 - rb_terms[t + 1] + next_values = rb_values[t + 1] + delta = ( + rb_rewards[t] + + args.gamma * next_values * next_non_terminal + - rb_values[t] + ) + rb_advantages[t] = last_gae_lam = ( + delta + + args.gamma + * args.gae_lambda + * next_non_terminal + * last_gae_lam + ) + rb_returns = rb_advantages + rb_values + + b_obs = torch.flatten(rb_obs[: args.rollout_length], start_dim=0, end_dim=1) + b_logprobs = torch.flatten( + rb_logprobs[: args.rollout_length], start_dim=0, end_dim=1 + ) + b_actions = torch.flatten( + rb_actions[: args.rollout_length], start_dim=0, end_dim=1 + ) + b_returns = torch.flatten( + rb_returns[: args.rollout_length], start_dim=0, end_dim=1 + ) + b_values = torch.flatten( + rb_values[: args.rollout_length], start_dim=0, end_dim=1 + ) + b_advantages = torch.flatten( + rb_advantages[: args.rollout_length], start_dim=0, end_dim=1 + ) + + b_index = np.arange(len(b_obs)) + clip_fracs = [] + for repeat in range(args.epochs): + np.random.shuffle(b_index) + for start in range(0, len(b_obs), args.batch_size): + # select the indices we want to train on + end = start + args.batch_size + batch_index = b_index[start:end] + + _, newlogprob, entropy, value = agent.get_action_and_value( + b_obs[batch_index], b_actions.long()[batch_index] + ) + logratio = newlogprob - b_logprobs[batch_index] + ratio = logratio.exp() + + with torch.no_grad(): + # calculate approx_kl http://joschu.net/blog/kl-approx.html + old_approx_kl = (-logratio).mean() + approx_kl = ((ratio - 1) - logratio).mean() + clip_fracs += [ + ((ratio - 1.0).abs() > args.clip_coef).float().mean().item() + ] + + # normalize advantages + rb_advantages = b_advantages[batch_index] + rb_advantages = (rb_advantages - rb_advantages.mean()) / ( + rb_advantages.std() + 1e-8 + ) + + # Policy loss + pg_loss1 = -b_advantages[batch_index] * ratio + pg_loss2 = -b_advantages[batch_index] * torch.clamp( + ratio, 1 - args.clip_coef, 1 + args.clip_coef + ) + pg_loss = torch.max(pg_loss1, pg_loss2).mean() + + # Value loss + value = value.flatten() + v_loss_unclipped = (value - b_returns[batch_index]) ** 2 + v_clipped = b_values[batch_index] + torch.clamp( + value - b_values[batch_index], + -args.clip_coef, + args.clip_coef, + ) + v_loss_clipped = (v_clipped - b_returns[batch_index]) ** 2 + v_loss_max = torch.max(v_loss_unclipped, v_loss_clipped) + v_loss = 0.5 * v_loss_max.mean() + + entropy_loss = entropy.mean() + loss = ( + pg_loss - args.ent_coef * entropy_loss + v_loss * args.vf_coef + ) + + optimizer.zero_grad() + loss.backward() + optimizer.step() + + writer.add_scalar("losses/value_loss", v_loss.item(), n_updates) + writer.add_scalar("losses/policy_loss", pg_loss.item(), n_updates) + writer.add_scalar("losses/entropy", entropy_loss.item(), n_updates) + writer.add_scalar( + "losses/old_approx_kl", old_approx_kl.item(), n_updates + ) + writer.add_scalar("losses/approx_kl", approx_kl.item(), n_updates) + n_updates += 1 + pbar.update(1) + + # update opponent + if args.agent_curriculum in ["FSP", "PFSP"]: + if ( + n_updates % args.agent_update_frequency == 0 + and episode != 0 + ): + mp_curriculum.update_agent(agent) + + y_pred, y_true = b_values.cpu().numpy(), b_returns.cpu().numpy() + var_y = np.var(y_true) + explained_var = ( + np.nan if var_y == 0 else 1 - np.var(y_true - y_pred) / var_y + ) + + if args.track: + # agent tasks + fig = px.histogram(agent_tasks, height=400) + fig.update_layout(bargap=0.2) + fig.update_layout(showlegend=False) + wandb.log({"charts/agent_tasks": wandb.Html(plotly.io.to_html(fig))}) + + # env tasks + fig = px.histogram(env_tasks, height=400) + fig.update_layout(bargap=0.2) + fig.update_layout(showlegend=False) + wandb.log({"charts/env_tasks": wandb.Html(plotly.io.to_html(fig))}) + + learner_winrate = n_learner_wins / episode + wandb.run.summary["learner_winrate"] = learner_winrate + writer.add_scalar("charts/learner_winrate", learner_winrate) + + # agent rewards + fig = px.line( + pd.DataFrame(rewards_history).cumsum(), + title="Agent rewards", + labels={"index": "Episodes", "value": "Cumulative rewards"}, + ) + wandb.log({"charts/agent_rewards": wandb.Html(plotly.io.to_html(fig))}) + + # win rates and replays + if args.agent_curriculum in ["FSP", "PFSP"]: + agent_ids = np.arange(agent_curriculum_settings["max_agents"]) + values = list(mp_curriculum.curriculum.agent_curriculum.history.values()) + winrates = [i["winrate"] for i in values] + n_games = [i["n_games"] for i in values] + + fig = make_subplots( + rows=2, cols=1, subplot_titles=("Win Rate", "Number of Games") + ) + fig.add_trace( + go.Bar(x=agent_ids, y=winrates, name="Win Rate", marker_color="blue"), + row=1, + col=1, + ) + fig.add_trace( + go.Bar( + x=agent_ids, + y=n_games, + name="Number of Games", + marker_color="orange", + ), + row=2, + col=1, + ) + + fig.update_yaxes(range=[0, 1], row=1, col=1) + fig.update_layout(showlegend=False) + wandb.log({"charts/opponent_winrates": wandb.Html(plotly.io.to_html(fig))}) + + writer.close() diff --git a/syllabus/examples/experimental/lasertag_rnn_lstm.ipynb b/syllabus/examples/experimental/lasertag_rnn_lstm.ipynb new file mode 100644 index 00000000..28db5f9c --- /dev/null +++ b/syllabus/examples/experimental/lasertag_rnn_lstm.ipynb @@ -0,0 +1,785 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "import argparse\n", + "import os\n", + "import sys\n", + "import time\n", + "from typing import Dict, Tuple, TypeVar\n", + "\n", + "import joblib\n", + "import numpy as np\n", + "import pandas as pd\n", + "import plotly\n", + "import plotly.express as px\n", + "import plotly.graph_objects as go\n", + "import torch\n", + "import torch.nn as nn\n", + "import torch.optim as optim\n", + "import wandb\n", + "from gymnasium import spaces\n", + "from plotly.subplots import make_subplots\n", + "from torch.distributions.categorical import Categorical\n", + "from torch.utils.tensorboard import SummaryWriter\n", + "from tqdm.auto import tqdm\n", + "from dataclasses import dataclass\n", + "\n", + "sys.path.append(\"../../..\")\n", + "from lasertag import LasertagAdversarial # noqa: E402\n", + "from syllabus.core import ( # noqa: E402\n", + " DualCurriculumWrapper,\n", + " TaskWrapper,\n", + " make_multiprocessing_curriculum,\n", + ")\n", + "\n", + "# noqa: E402\n", + "from syllabus.curricula import ( # noqa: E402\n", + " CentralizedPrioritizedLevelReplay,\n", + " DomainRandomization,\n", + " FictitiousSelfPlay,\n", + " PrioritizedFictitiousSelfPlay,\n", + " SelfPlay,\n", + ")\n", + "from syllabus.task_space import TaskSpace # noqa: E402\n", + "\n", + "ActionType = TypeVar(\"ActionType\")\n", + "AgentID = TypeVar(\"AgentID\")\n", + "AgentType = TypeVar(\"AgentType\")\n", + "EnvTask = TypeVar(\"EnvTask\")\n", + "AgentTask = TypeVar(\"AgentTask\")\n", + "ObsType = TypeVar(\"ObsType\")" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "@dataclass\n", + "class Args:\n", + " track: bool = False\n", + " total_updates: int = 4000\n", + " rollout_length: int = 256\n", + " batch_size: int = 32\n", + " ent_coef: float = 0.0\n", + " vf_coef: float = 0.5\n", + " clip_coef: float = 0.2\n", + " learning_rate: float = 1e-4\n", + " epsilon: float = 1e-5\n", + " gamma: float = 0.995\n", + " gae_lambda: float = 0.95\n", + " epochs: int = 5\n", + " agent_curriculum: str = \"SP\"\n", + " env_curriculum: str = \"DR\"\n", + " agent_update_frequency: int = 8000\n", + " max_agents: int = 10\n", + " save_agent_checkpoints: bool = False\n", + " checkpoint_frequency: int = 4000\n", + " n_env_tasks: int = 4000\n", + " seed: int = 0" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "def batchify(x, device):\n", + " \"\"\"Converts PZ style returns to batch of torch arrays.\"\"\"\n", + " # convert to list of np arrays\n", + " x = np.stack([x[a] for a in x], axis=0)\n", + " # convert to torch\n", + " x = torch.tensor(x).to(device)\n", + "\n", + " return x\n", + "\n", + "\n", + "def unbatchify(x, possible_agents: np.ndarray):\n", + " \"\"\"Converts np array to PZ style arguments.\"\"\"\n", + " x = x.cpu().numpy()\n", + " x = {agent: x[idx] for idx, agent in enumerate(possible_agents)}\n", + "\n", + " return x\n", + "\n", + "\n", + "class LasertagParallelWrapper(TaskWrapper):\n", + " \"\"\"\n", + " Wrapper ensuring compatibility with the PettingZoo Parallel API.\n", + "\n", + " Lasertag Environment:\n", + " * Action shape: `n_agents` * `Discrete(5)`\n", + " * Observation shape: Dict('image': Box(0, 255, (`n_agents`, 3, 5, 5), uint8))\n", + " \"\"\"\n", + "\n", + " def __init__(self, n_agents, *args, **kwargs):\n", + " super().__init__(*args, **kwargs)\n", + " self.n_agents = n_agents\n", + " self.task = None\n", + " self.episode_return = 0\n", + " self.possible_agents = [f\"agent_{i}\" for i in range(self.n_agents)]\n", + " self.n_steps = 0\n", + "\n", + " def __getattr__(self, name):\n", + " \"\"\"\n", + " Delegate attribute lookup to the wrapped environment if the attribute\n", + " is not found in the LasertagParallelWrapper instance.\n", + " \"\"\"\n", + " return getattr(self.env, name)\n", + "\n", + " def _np_array_to_pz_dict(self, array: np.ndarray) -> Dict[str, np.ndarray]:\n", + " \"\"\"\n", + " Returns a dictionary containing individual observations for each agent.\n", + " Assumes that the batch dimension represents individual agents.\n", + " \"\"\"\n", + " out = {}\n", + " for idx, value in enumerate(array):\n", + " out[self.possible_agents[idx]] = value\n", + " return out\n", + "\n", + " def _singleton_to_pz_dict(self, value: bool) -> Dict[str, bool]:\n", + " \"\"\"\n", + " Broadcasts the `done` and `trunc` flags to dictionaries keyed by agent id.\n", + " \"\"\"\n", + " return {str(agent_index): value for agent_index in range(self.n_agents)}\n", + "\n", + " def reset(\n", + " self, env_task: int\n", + " ) -> Tuple[Dict[AgentID, ObsType], Dict[AgentID, dict]]:\n", + " \"\"\"\n", + " Resets the environment and returns a dictionary of observations\n", + " keyed by agent ID.\n", + " \"\"\"\n", + " self.env.seed(env_task)\n", + " obs = self.env.reset_random() # random level generation\n", + " pz_obs = self._np_array_to_pz_dict(obs[\"image\"])\n", + "\n", + " return pz_obs\n", + "\n", + " def step(\n", + " self, action: Dict[AgentID, ActionType], device: str, agent_task: int\n", + " ) -> Tuple[\n", + " Dict[AgentID, ObsType],\n", + " Dict[AgentID, float],\n", + " Dict[AgentID, bool],\n", + " Dict[AgentID, bool],\n", + " Dict[AgentID, dict],\n", + " ]:\n", + " \"\"\"\n", + " Takes inputs in the PettingZoo (PZ) Parallel API format, performs a step and\n", + " returns outputs in PZ format.\n", + " \"\"\"\n", + " action = batchify(action, device)\n", + " obs, rew, done, info = self.env.step(action)\n", + " obs = obs[\"image\"]\n", + " trunc = False # there is no `truncated` flag in this environment\n", + " self.task_completion = self._task_completion(obs, rew, done, trunc, info)\n", + " # convert outputs back to PZ format\n", + " obs, rew = map(self._np_array_to_pz_dict, [obs, rew])\n", + " done, trunc, info = map(\n", + " self._singleton_to_pz_dict, [done, trunc, self.task_completion]\n", + " )\n", + " info[\"agent_id\"] = agent_task\n", + " self.n_steps += 1\n", + "\n", + " return self.observation(obs), rew, done, trunc, info" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "- convolution layer (3 × 3 kernel, stride length 1, 16 filters)\n", + "- flatten\n", + "- Relu\n", + "- LSTM (256)\n", + "- Dense(32)\n", + "- Relu\n", + "- Dense(32)\n", + "- Relu\n", + " => logits over 5 actions\n", + "\n", + "### TODO:\n", + "\n", + "**_NO AGENT DIRECTIONS AS INPUTS_**\n" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "class Agent(nn.Module):\n", + " def __init__(self, input_channels: int, num_actions: int) -> None:\n", + " super().__init__()\n", + "\n", + " self.conv = nn.Sequential(\n", + " self.layer_init(\n", + " nn.Conv2d(input_channels, out_channels=16, kernel_size=3, stride=1)\n", + " ),\n", + " nn.ReLU(),\n", + " )\n", + " self.lstm = nn.LSTM(input_size=16 * 3 * 3, hidden_size=256, batch_first=True)\n", + " self.lstm_init()\n", + "\n", + " self.mlp = nn.Sequential(\n", + " nn.ReLU(),\n", + " self.layer_init(nn.Linear(256, 32)),\n", + " nn.ReLU(),\n", + " )\n", + " # TODO: the paper doesn't mention the critic network?\n", + " self.actor = self.layer_init(nn.Linear(32, num_actions), scale=0.01)\n", + " self.critic = self.layer_init(nn.Linear(32, 1), scale=1)\n", + "\n", + " def get_states(self, x, lstm_state, done):\n", + " # add batch dim if missing\n", + " if len(x.shape) == 3:\n", + " x = x.unsqueeze(0) # shape: batch_size, *obs_shape\n", + "\n", + " hidden = self.conv(x / 255.0) # shape: batch_size, n_features, kernel, kernel\n", + "\n", + " batch_size = hidden.size(0)\n", + " hidden = hidden.reshape(batch_size, -1) # shape: batch_size, features\n", + " hidden = hidden.unsqueeze(1) # add seq len dimension\n", + " # => shape: batch_size, seq_len=1, n_features\n", + "\n", + " new_hidden = []\n", + " # reset lstm state if done\n", + " for h, d in zip(hidden, done):\n", + " h, lstm_state = self.lstm(\n", + " h.unsqueeze(0),\n", + " (\n", + " torch.logical_not(d).view(1, -1, 1) * lstm_state[0],\n", + " torch.logical_not(d).view(1, -1, 1) * lstm_state[1],\n", + " ),\n", + " )\n", + " new_hidden += [h]\n", + "\n", + " new_hidden = torch.flatten(torch.cat(new_hidden), 0, 1)\n", + " return new_hidden, lstm_state\n", + "\n", + " def get_value(self, x, lstm_state, done):\n", + " hidden, _ = self.get_states(x, lstm_state, done)\n", + " hidden = self.mlp(hidden)\n", + " return self.critic(hidden)\n", + "\n", + " def get_action_and_value(self, x, lstm_state, done, action=None):\n", + " hidden, lstm_state = self.get_states(x, lstm_state, done)\n", + " hidden = self.mlp(hidden)\n", + " logits = self.actor(hidden)\n", + " probs = Categorical(logits=logits)\n", + "\n", + " if action is None:\n", + " action = probs.sample()\n", + "\n", + " return (\n", + " action,\n", + " probs.log_prob(action),\n", + " probs.entropy(),\n", + " self.critic(hidden),\n", + " lstm_state,\n", + " )\n", + "\n", + " def layer_init(self, layer, scale=np.sqrt(2), bias_const=0.0):\n", + " torch.nn.init.orthogonal_(layer.weight, scale)\n", + " torch.nn.init.constant_(layer.bias, bias_const)\n", + " return layer\n", + "\n", + " def lstm_init(self):\n", + " for name, param in self.lstm.named_parameters():\n", + " if \"bias\" in name:\n", + " nn.init.constant_(param, 0)\n", + " elif \"weight\" in name:\n", + " nn.init.orthogonal_(param, 1.0)" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "agent_curriculums = {\n", + " \"SP\": SelfPlay,\n", + " \"FSP\": FictitiousSelfPlay,\n", + " \"PFSP\": PrioritizedFictitiousSelfPlay,\n", + "}\n", + "env_curriculums = {\n", + " \"DR\": DomainRandomization,\n", + " \"PLR\": CentralizedPrioritizedLevelReplay,\n", + "}\n", + "\n", + "if __name__ == \"__main__\":\n", + " args = Args()\n", + " args.agent_curriculum = \"PFSP\"\n", + " args.env_curriculum = \"PLR\"\n", + " np.random.seed(args.seed)\n", + "\n", + " \"\"\"ALGO PARAMS\"\"\"\n", + " device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n", + "\n", + " stack_size = 3\n", + " frame_size = (5, 5)\n", + " n_agents = 2\n", + " num_actions = 5\n", + "\n", + " agent_curriculum_settings = {\n", + " \"device\": device,\n", + " \"storage_path\": f\"{args.agent_curriculum}_agents\",\n", + " \"max_agents\": args.max_agents,\n", + " \"seed\": args.seed,\n", + " }\n", + "\n", + " env_task_space = TaskSpace(spaces.Discrete(args.n_env_tasks))\n", + " env_curriculum_settings = {\n", + " \"DR\": {\"task_space\": env_task_space},\n", + " \"PLR\": {\n", + " \"task_space\": env_task_space,\n", + " \"num_steps\": args.rollout_length,\n", + " \"num_processes\": 1, # TODO: modify if using vecenvs\n", + " \"gamma\": args.gamma,\n", + " \"gae_lambda\": args.gae_lambda,\n", + " \"task_sampler_kwargs_dict\": {\"strategy\": \"value_l1\"},\n", + " },\n", + " }\n", + "\n", + " \"\"\" LEARNER SETUP \"\"\"\n", + " agent = Agent(input_channels=stack_size, num_actions=num_actions).to(device)\n", + " optimizer = optim.Adam(agent.parameters(), lr=args.learning_rate, eps=args.epsilon)\n", + "\n", + " \"\"\" ENV SETUP \"\"\"\n", + " env = LasertagAdversarial(record_video=False) # 2 agents by default\n", + " env = LasertagParallelWrapper(env=env, n_agents=n_agents)\n", + " agent_curriculum = agent_curriculums[args.agent_curriculum](\n", + " agent=agent, **agent_curriculum_settings\n", + " )\n", + "\n", + " env_curriculum = env_curriculums[args.env_curriculum](\n", + " **env_curriculum_settings[args.env_curriculum]\n", + " )\n", + "\n", + " curriculum = DualCurriculumWrapper(\n", + " env=env,\n", + " agent_curriculum=agent_curriculum,\n", + " env_curriculum=env_curriculum,\n", + " )\n", + " \n", + " \"\"\" ALGO LOGIC: EPISODE STORAGE\"\"\"\n", + " total_episodic_return = 0\n", + " rb_obs = torch.zeros((args.rollout_length, n_agents, stack_size, *frame_size)).to(\n", + " device\n", + " )\n", + " rb_actions = torch.zeros((args.rollout_length, n_agents)).to(device)\n", + " rb_logprobs = torch.zeros((args.rollout_length, n_agents)).to(device)\n", + " rb_rewards = torch.zeros((args.rollout_length, n_agents)).to(device)\n", + " rb_terms = torch.zeros((args.rollout_length, n_agents)).to(device)\n", + " rb_values = torch.zeros((args.rollout_length, n_agents)).to(device)\n", + "\n", + " agent_tasks, env_tasks, rewards_history = [], [], []\n", + " agent_c_rew, opp_c_rew = 0, 0\n", + " episode, n_learner_wins = 0, 0\n", + " n_updates = 0\n", + " info = {}" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [], + "source": [ + "dones = {\n", + " \"agent_0\": 0,\n", + " \"agent_1\": 0,\n", + "}\n", + "lstm_state = (\n", + " torch.zeros(agent.lstm.num_layers, 1, agent.lstm.hidden_size).to(device),\n", + " torch.zeros(agent.lstm.num_layers, 1, agent.lstm.hidden_size).to(device),\n", + ")\n", + "lstm_state_opponent = (\n", + " torch.zeros(agent.lstm.num_layers, 1, agent.lstm.hidden_size).to(device),\n", + " torch.zeros(agent.lstm.num_layers, 1, agent.lstm.hidden_size).to(device),\n", + ")\n" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "training ...\n" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "df07e6403dbd433e98945b9b1794b07f", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + " 0%| | 0/4000 [00:00 25\u001b[0m \u001b[43magent\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mget_action_and_value\u001b[49m\u001b[43m(\u001b[49m\n\u001b[0;32m 26\u001b[0m \u001b[43m \u001b[49m\u001b[43magent_obs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mlstm_state\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mbatchify\u001b[49m\u001b[43m(\u001b[49m\u001b[43mdones\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mdevice\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 27\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 28\u001b[0m )\n\u001b[0;32m 30\u001b[0m opponent \u001b[38;5;241m=\u001b[39m curriculum\u001b[38;5;241m.\u001b[39mget_opponent(info\u001b[38;5;241m.\u001b[39mget(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124magent_id\u001b[39m\u001b[38;5;124m\"\u001b[39m, \u001b[38;5;241m0\u001b[39m))\u001b[38;5;241m.\u001b[39mto(\n\u001b[0;32m 31\u001b[0m device\n\u001b[0;32m 32\u001b[0m )\n\u001b[0;32m 33\u001b[0m opponent_action, _, _, _, lstm_state_opponent \u001b[38;5;241m=\u001b[39m (\n\u001b[0;32m 34\u001b[0m opponent\u001b[38;5;241m.\u001b[39mget_action_and_value(\n\u001b[0;32m 35\u001b[0m opponent_obs, lstm_state_opponent, batchify(dones, device)\n\u001b[0;32m 36\u001b[0m )\n\u001b[0;32m 37\u001b[0m )\n", + "Cell \u001b[1;32mIn[10], line 56\u001b[0m, in \u001b[0;36mAgent.get_action_and_value\u001b[1;34m(self, x, lstm_state, done, action)\u001b[0m\n\u001b[0;32m 55\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mget_action_and_value\u001b[39m(\u001b[38;5;28mself\u001b[39m, x, lstm_state, done, action\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mNone\u001b[39;00m):\n\u001b[1;32m---> 56\u001b[0m hidden, lstm_state \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mget_states\u001b[49m\u001b[43m(\u001b[49m\u001b[43mx\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mlstm_state\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mdone\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 57\u001b[0m hidden \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mmlp(hidden)\n\u001b[0;32m 58\u001b[0m logits \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mactor(hidden)\n", + "Cell \u001b[1;32mIn[10], line 28\u001b[0m, in \u001b[0;36mAgent.get_states\u001b[1;34m(self, x, lstm_state, done)\u001b[0m\n\u001b[0;32m 25\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mlen\u001b[39m(x\u001b[38;5;241m.\u001b[39mshape) \u001b[38;5;241m==\u001b[39m \u001b[38;5;241m3\u001b[39m:\n\u001b[0;32m 26\u001b[0m x \u001b[38;5;241m=\u001b[39m x\u001b[38;5;241m.\u001b[39munsqueeze(\u001b[38;5;241m0\u001b[39m) \u001b[38;5;66;03m# shape: batch_size, *obs_shape\u001b[39;00m\n\u001b[1;32m---> 28\u001b[0m hidden \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mconv\u001b[49m\u001b[43m(\u001b[49m\u001b[43mx\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m/\u001b[39;49m\u001b[43m \u001b[49m\u001b[38;5;241;43m255.0\u001b[39;49m\u001b[43m)\u001b[49m \u001b[38;5;66;03m# shape: batch_size, n_features, kernel, kernel\u001b[39;00m\n\u001b[0;32m 30\u001b[0m batch_size \u001b[38;5;241m=\u001b[39m hidden\u001b[38;5;241m.\u001b[39msize(\u001b[38;5;241m0\u001b[39m)\n\u001b[0;32m 31\u001b[0m hidden \u001b[38;5;241m=\u001b[39m hidden\u001b[38;5;241m.\u001b[39mreshape(batch_size, \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m1\u001b[39m) \u001b[38;5;66;03m# shape: batch_size, features\u001b[39;00m\n", + "File \u001b[1;32mc:\\Users\\ryanp\\AppData\\Local\\Programs\\Python\\Python310\\lib\\site-packages\\torch\\nn\\modules\\module.py:1532\u001b[0m, in \u001b[0;36mModule._wrapped_call_impl\u001b[1;34m(self, *args, **kwargs)\u001b[0m\n\u001b[0;32m 1530\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_compiled_call_impl(\u001b[38;5;241m*\u001b[39margs, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs) \u001b[38;5;66;03m# type: ignore[misc]\u001b[39;00m\n\u001b[0;32m 1531\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[1;32m-> 1532\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_call_impl(\u001b[38;5;241m*\u001b[39margs, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs)\n", + "File \u001b[1;32mc:\\Users\\ryanp\\AppData\\Local\\Programs\\Python\\Python310\\lib\\site-packages\\torch\\nn\\modules\\module.py:1541\u001b[0m, in \u001b[0;36mModule._call_impl\u001b[1;34m(self, *args, **kwargs)\u001b[0m\n\u001b[0;32m 1536\u001b[0m \u001b[38;5;66;03m# If we don't have any hooks, we want to skip the rest of the logic in\u001b[39;00m\n\u001b[0;32m 1537\u001b[0m \u001b[38;5;66;03m# this function, and just call forward.\u001b[39;00m\n\u001b[0;32m 1538\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m (\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_backward_hooks \u001b[38;5;129;01mor\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_backward_pre_hooks \u001b[38;5;129;01mor\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_forward_hooks \u001b[38;5;129;01mor\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_forward_pre_hooks\n\u001b[0;32m 1539\u001b[0m \u001b[38;5;129;01mor\u001b[39;00m _global_backward_pre_hooks \u001b[38;5;129;01mor\u001b[39;00m _global_backward_hooks\n\u001b[0;32m 1540\u001b[0m \u001b[38;5;129;01mor\u001b[39;00m _global_forward_hooks \u001b[38;5;129;01mor\u001b[39;00m _global_forward_pre_hooks):\n\u001b[1;32m-> 1541\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m forward_call(\u001b[38;5;241m*\u001b[39margs, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs)\n\u001b[0;32m 1543\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[0;32m 1544\u001b[0m result \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mNone\u001b[39;00m\n", + "File \u001b[1;32mc:\\Users\\ryanp\\AppData\\Local\\Programs\\Python\\Python310\\lib\\site-packages\\torch\\nn\\modules\\container.py:217\u001b[0m, in \u001b[0;36mSequential.forward\u001b[1;34m(self, input)\u001b[0m\n\u001b[0;32m 215\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mforward\u001b[39m(\u001b[38;5;28mself\u001b[39m, \u001b[38;5;28minput\u001b[39m):\n\u001b[0;32m 216\u001b[0m \u001b[38;5;28;01mfor\u001b[39;00m module \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28mself\u001b[39m:\n\u001b[1;32m--> 217\u001b[0m \u001b[38;5;28minput\u001b[39m \u001b[38;5;241m=\u001b[39m \u001b[43mmodule\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;28;43minput\u001b[39;49m\u001b[43m)\u001b[49m\n\u001b[0;32m 218\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28minput\u001b[39m\n", + "File \u001b[1;32mc:\\Users\\ryanp\\AppData\\Local\\Programs\\Python\\Python310\\lib\\site-packages\\torch\\nn\\modules\\module.py:1532\u001b[0m, in \u001b[0;36mModule._wrapped_call_impl\u001b[1;34m(self, *args, **kwargs)\u001b[0m\n\u001b[0;32m 1530\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_compiled_call_impl(\u001b[38;5;241m*\u001b[39margs, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs) \u001b[38;5;66;03m# type: ignore[misc]\u001b[39;00m\n\u001b[0;32m 1531\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[1;32m-> 1532\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_call_impl(\u001b[38;5;241m*\u001b[39margs, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs)\n", + "File \u001b[1;32mc:\\Users\\ryanp\\AppData\\Local\\Programs\\Python\\Python310\\lib\\site-packages\\torch\\nn\\modules\\module.py:1541\u001b[0m, in \u001b[0;36mModule._call_impl\u001b[1;34m(self, *args, **kwargs)\u001b[0m\n\u001b[0;32m 1536\u001b[0m \u001b[38;5;66;03m# If we don't have any hooks, we want to skip the rest of the logic in\u001b[39;00m\n\u001b[0;32m 1537\u001b[0m \u001b[38;5;66;03m# this function, and just call forward.\u001b[39;00m\n\u001b[0;32m 1538\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m (\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_backward_hooks \u001b[38;5;129;01mor\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_backward_pre_hooks \u001b[38;5;129;01mor\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_forward_hooks \u001b[38;5;129;01mor\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_forward_pre_hooks\n\u001b[0;32m 1539\u001b[0m \u001b[38;5;129;01mor\u001b[39;00m _global_backward_pre_hooks \u001b[38;5;129;01mor\u001b[39;00m _global_backward_hooks\n\u001b[0;32m 1540\u001b[0m \u001b[38;5;129;01mor\u001b[39;00m _global_forward_hooks \u001b[38;5;129;01mor\u001b[39;00m _global_forward_pre_hooks):\n\u001b[1;32m-> 1541\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m forward_call(\u001b[38;5;241m*\u001b[39margs, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs)\n\u001b[0;32m 1543\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[0;32m 1544\u001b[0m result \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mNone\u001b[39;00m\n", + "File \u001b[1;32mc:\\Users\\ryanp\\AppData\\Local\\Programs\\Python\\Python310\\lib\\site-packages\\torch\\nn\\modules\\conv.py:460\u001b[0m, in \u001b[0;36mConv2d.forward\u001b[1;34m(self, input)\u001b[0m\n\u001b[0;32m 459\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mforward\u001b[39m(\u001b[38;5;28mself\u001b[39m, \u001b[38;5;28minput\u001b[39m: Tensor) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m Tensor:\n\u001b[1;32m--> 460\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_conv_forward\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;28;43minput\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mweight\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mbias\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[1;32mc:\\Users\\ryanp\\AppData\\Local\\Programs\\Python\\Python310\\lib\\site-packages\\torch\\nn\\modules\\conv.py:456\u001b[0m, in \u001b[0;36mConv2d._conv_forward\u001b[1;34m(self, input, weight, bias)\u001b[0m\n\u001b[0;32m 452\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mpadding_mode \u001b[38;5;241m!=\u001b[39m \u001b[38;5;124m'\u001b[39m\u001b[38;5;124mzeros\u001b[39m\u001b[38;5;124m'\u001b[39m:\n\u001b[0;32m 453\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m F\u001b[38;5;241m.\u001b[39mconv2d(F\u001b[38;5;241m.\u001b[39mpad(\u001b[38;5;28minput\u001b[39m, \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_reversed_padding_repeated_twice, mode\u001b[38;5;241m=\u001b[39m\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mpadding_mode),\n\u001b[0;32m 454\u001b[0m weight, bias, \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mstride,\n\u001b[0;32m 455\u001b[0m _pair(\u001b[38;5;241m0\u001b[39m), \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mdilation, \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mgroups)\n\u001b[1;32m--> 456\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mF\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mconv2d\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;28;43minput\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mweight\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mbias\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mstride\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 457\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mpadding\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mdilation\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mgroups\u001b[49m\u001b[43m)\u001b[49m\n", + "\u001b[1;31mRuntimeError\u001b[0m: Input type (torch.cuda.FloatTensor) and weight type (torch.FloatTensor) should be the same" + ] + } + ], + "source": [ + "\"\"\" TRAINING LOGIC \"\"\"\n", + "print(\"training ...\")\n", + "with tqdm(total=args.total_updates) as pbar:\n", + " while n_updates < args.total_updates:\n", + "\n", + " initial_lstm_state = (lstm_state[0].clone(), lstm_state[1].clone())\n", + " initial_lstm_state_opponent = (\n", + " lstm_state_opponent[0].clone(),\n", + " lstm_state_opponent[1].clone(),\n", + " )\n", + " with torch.no_grad():\n", + " env_task, agent_task = curriculum.sample()\n", + "\n", + " env_tasks.append(env_task)\n", + " agent_tasks.append(agent_task)\n", + "\n", + " next_obs = env.reset(env_task)\n", + " total_episodic_return = 0\n", + "\n", + " for step in range(0, args.rollout_length):\n", + " joint_obs = batchify(next_obs, device).squeeze()\n", + " agent_obs, opponent_obs = joint_obs\n", + "\n", + " actions, logprobs, _, values, lstm_state = (\n", + " agent.get_action_and_value(\n", + " agent_obs, lstm_state, batchify(dones, device)\n", + " )\n", + " )\n", + "\n", + " opponent = curriculum.get_opponent(info.get(\"agent_id\", 0)).to(\n", + " device\n", + " )\n", + " opponent_action, _, _, _, lstm_state_opponent = (\n", + " opponent.get_action_and_value(\n", + " opponent_obs, lstm_state_opponent, batchify(dones, device)\n", + " )\n", + " )\n", + "\n", + " joint_actions = torch.tensor((actions, opponent_action))\n", + " next_obs, rewards, dones, truncs, info = env.step(\n", + " unbatchify(joint_actions, env.possible_agents),\n", + " device,\n", + " agent_task,\n", + " )\n", + "\n", + " opp_reward = rewards[\"agent_1\"]\n", + " if opp_reward != 0:\n", + " curriculum.update_winrate(info[\"agent_id\"], opp_reward)\n", + " if opp_reward == -1:\n", + " n_learner_wins += 1\n", + "\n", + " rb_obs[step] = batchify(next_obs, device)\n", + " rb_rewards[step] = batchify(rewards, device)\n", + " rb_terms[step] = batchify(dones, device)\n", + " rb_actions[step] = joint_actions\n", + " rb_logprobs[step] = logprobs\n", + " rb_values[step] = values.flatten()\n", + "\n", + " total_episodic_return += rb_rewards[step].cpu().numpy()\n", + "\n", + " agent_c_rew += rewards[\"agent_0\"]\n", + " opp_c_rew += rewards[\"agent_1\"]\n", + " grid_size = env.level[3][\"grid_size_selected\"]\n", + " walls_percentage = env.level[3][\"clutter_rate_selected\"]\n", + "\n", + " if any([dones[a] for a in dones]) or any(\n", + " [truncs[a] for a in truncs]\n", + " ):\n", + " episode += 1\n", + " rewards_history.append(rewards)\n", + " env_task, agent_task = curriculum.sample()\n", + " env_tasks.append(env_task)\n", + " agent_tasks.append(agent_task)\n", + "\n", + " \n", + " \n", + " learner_winrate = n_learner_wins / episode\n", + "\n", + " next_obs = env.reset(env_task)\n", + "\n", + " # store learner checkpoints\n", + " # if (\n", + " # args.save_agent_checkpoints\n", + " # and n_updates % args.checkpoint_frequency == 0\n", + " # ):\n", + " # print(f\"saving checkpoint --{n_updates}\")\n", + " # checkpoint_path = (\n", + " # f\"{args.logging_dir}/{run_name}_checkpoints/\"\n", + " # f\"{curriculum.env_curriculum.name}_\"\n", + " # f\"{curriculum.agent_curriculum.name}_{n_updates}\"\n", + " # f\"_seed_{args.seed}.pkl\"\n", + " # )\n", + " # # --- local checkpoint ---\n", + " # joblib.dump(\n", + " # agent,\n", + " # filename=checkpoint_path,\n", + " # )\n", + "\n", + " # --- wandb checkpoint ---\n", + " # if args.track:\n", + " # agent_artifact = wandb.Artifact(\"model\", type=\"model\")\n", + " # agent_artifact.add_file(checkpoint_path)\n", + " # wandb.log_artifact(agent_artifact)\n", + "\n", + " if args.env_curriculum == \"PLR\":\n", + " with torch.no_grad():\n", + " next_value = agent.get_value(\n", + " torch.tensor(next_obs[\"agent_0\"]).to(device),\n", + " lstm_state,\n", + " batchify(dones, device),\n", + " )\n", + " update = {\n", + " \"update_type\": \"on_demand\",\n", + " \"metrics\": {\n", + " \"value\": values,\n", + " \"next_value\": next_value,\n", + " \"rew\": rewards[\n", + " \"agent_0\"\n", + " ], # TODO: is this the expected use?\n", + " \"dones\": dones[0],\n", + " \"tasks\": env_task,\n", + " },\n", + " }\n", + "\n", + " # gae\n", + " with torch.no_grad():\n", + " next_value = agent.get_value(\n", + " torch.tensor(next_obs[\"agent_0\"]).to(device),\n", + " lstm_state,\n", + " batchify(dones, device),\n", + " )\n", + " rb_advantages = torch.zeros_like(rb_rewards).to(device)\n", + " last_gae_lam = 0\n", + " for t in reversed(range(args.rollout_length - 1)):\n", + " if t == args.rollout_length - 1:\n", + " next_non_terminal = 1.0 - rb_terms[t + 1]\n", + " next_values = next_value\n", + " else:\n", + " next_non_terminal = 1.0 - rb_terms[t + 1]\n", + " next_values = rb_values[t + 1]\n", + " delta = (\n", + " rb_rewards[t]\n", + " + args.gamma * next_values * next_non_terminal\n", + " - rb_values[t]\n", + " )\n", + " rb_advantages[t] = last_gae_lam = (\n", + " delta\n", + " + args.gamma\n", + " * args.gae_lambda\n", + " * next_non_terminal\n", + " * last_gae_lam\n", + " )\n", + " rb_returns = rb_advantages + rb_values\n", + "\n", + " b_obs = torch.flatten(rb_obs[: args.rollout_length], start_dim=0, end_dim=1)\n", + " b_logprobs = torch.flatten(\n", + " rb_logprobs[: args.rollout_length], start_dim=0, end_dim=1\n", + " )\n", + " b_actions = torch.flatten(\n", + " rb_actions[: args.rollout_length], start_dim=0, end_dim=1\n", + " )\n", + " b_returns = torch.flatten(\n", + " rb_returns[: args.rollout_length], start_dim=0, end_dim=1\n", + " )\n", + " b_values = torch.flatten(\n", + " rb_values[: args.rollout_length], start_dim=0, end_dim=1\n", + " )\n", + " b_advantages = torch.flatten(\n", + " rb_advantages[: args.rollout_length], start_dim=0, end_dim=1\n", + " )\n", + " b_terms = torch.flatten(\n", + " rb_terms[: args.rollout_length], start_dim=0, end_dim=1\n", + " )\n", + "\n", + " b_index = np.arange(len(b_obs))\n", + " clip_fracs = []\n", + " for repeat in range(args.epochs):\n", + " np.random.shuffle(b_index)\n", + " for start in range(0, len(b_obs), args.batch_size):\n", + " # select the indices we want to train on\n", + " end = start + args.batch_size\n", + " batch_index = b_index[start:end]\n", + "\n", + " _, newlogprob, entropy, value, _ = agent.get_action_and_value(\n", + " b_obs[batch_index],\n", + " (\n", + " initial_lstm_state[0],\n", + " initial_lstm_state[1],\n", + " ),\n", + " b_terms[batch_index],\n", + " b_actions.long()[batch_index],\n", + " )\n", + " logratio = newlogprob - b_logprobs[batch_index]\n", + " ratio = logratio.exp()\n", + "\n", + " with torch.no_grad():\n", + " # calculate approx_kl http://joschu.net/blog/kl-approx.html\n", + " old_approx_kl = (-logratio).mean()\n", + " approx_kl = ((ratio - 1) - logratio).mean()\n", + " clip_fracs += [\n", + " ((ratio - 1.0).abs() > args.clip_coef).float().mean().item()\n", + " ]\n", + "\n", + " # normalize advantages\n", + " rb_advantages = b_advantages[batch_index]\n", + " rb_advantages = (rb_advantages - rb_advantages.mean()) / (\n", + " rb_advantages.std() + 1e-8\n", + " )\n", + "\n", + " # Policy loss\n", + " pg_loss1 = -b_advantages[batch_index] * ratio\n", + " pg_loss2 = -b_advantages[batch_index] * torch.clamp(\n", + " ratio, 1 - args.clip_coef, 1 + args.clip_coef\n", + " )\n", + " pg_loss = torch.max(pg_loss1, pg_loss2).mean()\n", + "\n", + " # Value loss\n", + " value = value.flatten()\n", + " v_loss_unclipped = (value - b_returns[batch_index]) ** 2\n", + " v_clipped = b_values[batch_index] + torch.clamp(\n", + " value - b_values[batch_index],\n", + " -args.clip_coef,\n", + " args.clip_coef,\n", + " )\n", + " v_loss_clipped = (v_clipped - b_returns[batch_index]) ** 2\n", + " v_loss_max = torch.max(v_loss_unclipped, v_loss_clipped)\n", + " v_loss = 0.5 * v_loss_max.mean()\n", + "\n", + " entropy_loss = entropy.mean()\n", + " loss = (\n", + " pg_loss - args.ent_coef * entropy_loss + v_loss * args.vf_coef\n", + " )\n", + "\n", + " optimizer.zero_grad()\n", + " loss.backward()\n", + " optimizer.step()\n", + "\n", + " # writer.add_scalar(\"losses/value_loss\", v_loss.item(), n_updates)\n", + " # writer.add_scalar(\"losses/policy_loss\", pg_loss.item(), n_updates)\n", + " # writer.add_scalar(\"losses/entropy\", entropy_loss.item(), n_updates)\n", + " # writer.add_scalar(\n", + " # \"losses/old_approx_kl\", old_approx_kl.item(), n_updates\n", + " # )\n", + " # writer.add_scalar(\"losses/approx_kl\", approx_kl.item(), n_updates)\n", + " n_updates += 1\n", + " pbar.update(1)\n", + "\n", + " # update opponent\n", + " if args.agent_curriculum in [\"FSP\", \"PFSP\"]:\n", + " if (\n", + " n_updates % args.agent_update_frequency == 0\n", + " and episode != 0\n", + " ):\n", + " curriculum.update_agent(agent)\n", + "\n", + " y_pred, y_true = b_values.cpu().numpy(), b_returns.cpu().numpy()\n", + " var_y = np.var(y_true)\n", + " explained_var = (\n", + " np.nan if var_y == 0 else 1 - np.var(y_true - y_pred) / var_y\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'0': False, '1': False}" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "dones" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "ename": "NameError", + "evalue": "name 'curriculum' is not defined", + "output_type": "error", + "traceback": [ + "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[1;31mNameError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[1;32mIn[1], line 1\u001b[0m\n\u001b[1;32m----> 1\u001b[0m \u001b[43mcurriculum\u001b[49m\u001b[38;5;241m.\u001b[39magent_curriculum\u001b[38;5;241m.\u001b[39mname\n", + "\u001b[1;31mNameError\u001b[0m: name 'curriculum' is not defined" + ] + } + ], + "source": [ + "curriculum.agent_curriculum.name" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.7" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/syllabus/examples/experimental/lasertag_rnn_lstm.py b/syllabus/examples/experimental/lasertag_rnn_lstm.py new file mode 100644 index 00000000..fec14e72 --- /dev/null +++ b/syllabus/examples/experimental/lasertag_rnn_lstm.py @@ -0,0 +1,733 @@ +import argparse +import os +import sys +import time +from typing import Dict, Tuple, TypeVar + +import joblib +import numpy as np +import pandas as pd +import plotly +import plotly.express as px +import plotly.graph_objects as go +import torch +import torch.nn as nn +import torch.optim as optim +from gymnasium import spaces +from plotly.subplots import make_subplots +from torch.distributions.categorical import Categorical +from torch.utils.tensorboard import SummaryWriter +from tqdm.auto import tqdm + +import wandb + +sys.path.append("../../..") +from lasertag import LasertagAdversarial # noqa: E402 +from syllabus.core import DualCurriculumWrapper, TaskWrapper # noqa: E402 + +# noqa: E402 +from syllabus.curricula import ( # noqa: E402 + CentralizedPrioritizedLevelReplay, + DomainRandomization, + FictitiousSelfPlay, + PrioritizedFictitiousSelfPlay, + SelfPlay, +) +from syllabus.task_space import TaskSpace # noqa: E402 + +ActionType = TypeVar("ActionType") +AgentID = TypeVar("AgentID") +AgentType = TypeVar("AgentType") +EnvTask = TypeVar("EnvTask") +AgentTask = TypeVar("AgentTask") +ObsType = TypeVar("ObsType") + + +def parse_args(): + parser = argparse.ArgumentParser() + parser.add_argument("--track", type=bool, default=False) + parser.add_argument("--total-updates", type=int, default=40000) + parser.add_argument("--rollout-length", type=int, default=256) + parser.add_argument( + "--batch-size", type=int, default=32 + ) # TODO: determine correct value + + # PPO args + parser.add_argument("--ent-coef", type=float, default=0.0) + parser.add_argument("--vf-coef", type=float, default=0.5) + parser.add_argument("--clip-coef", type=float, default=0.2) + parser.add_argument("--learning-rate", type=float, default=1e-4) + parser.add_argument("--epsilon", type=float, default=1e-5) + parser.add_argument("--gamma", type=float, default=0.995) + parser.add_argument("--gae-lambda", type=float, default=0.95) + parser.add_argument("--epochs", type=int, default=5) + + # curriculum args + parser.add_argument( + "--agent-curriculum", type=str, default="SP", choices=["SP", "FSP", "PFSP"] + ) + parser.add_argument( + "--env-curriculum", type=str, default="DR", choices=["DR", "PLR"] + ) + parser.add_argument("--agent-update-frequency", type=int, default=8000) + # number of opponents in FSP and PFSP + parser.add_argument("--max-agents", type=int, default=10) + parser.add_argument("--save-agent-checkpoints", type=bool, default=False) + parser.add_argument( + "--checkpoint-frequency", type=int, default=4000 + ) # agent checkpoints every N steps + parser.add_argument("--n-env-tasks", type=int, default=4000) + parser.add_argument("--seed", type=int, default=0) + parser.add_argument( + "--exp-name", + type=str, + default="lasertag_", + help="the name of this experiment", + ) + parser.add_argument( + "--wandb-project-name", + type=str, + default="syllabus", + help="the wandb's project name", + ) + parser.add_argument( + "--wandb-entity", + type=str, + default="rpegoud", + help="the entity (team) of wandb's project", + ) + parser.add_argument( + "--logging-dir", + type=str, + default=".", + help="the base directory for logging and wandb storage.", + ) + + args = parser.parse_args() + return args + + +def batchify(x, device): + """Converts PZ style returns to batch of torch arrays.""" + # convert to list of np arrays + x = np.stack([x[a] for a in x], axis=0) + # convert to torch + x = torch.tensor(x).to(device) + + return x + + +def unbatchify(x, possible_agents: np.ndarray): + """Converts np array to PZ style arguments.""" + x = x.cpu().numpy() + x = {agent: x[idx] for idx, agent in enumerate(possible_agents)} + + return x + + +class LasertagParallelWrapper(TaskWrapper): + """ + Wrapper ensuring compatibility with the PettingZoo Parallel API. + + Lasertag Environment: + * Action shape: `n_agents` * `Discrete(5)` + * Observation shape: Dict('image': Box(0, 255, (`n_agents`, 3, 5, 5), uint8)) + """ + + def __init__(self, n_agents, *args, **kwargs): + super().__init__(*args, **kwargs) + self.n_agents = n_agents + self.task = None + self.episode_return = 0 + self.possible_agents = [f"agent_{i}" for i in range(self.n_agents)] + self.n_steps = 0 + + def __getattr__(self, name): + """ + Delegate attribute lookup to the wrapped environment if the attribute + is not found in the LasertagParallelWrapper instance. + """ + return getattr(self.env, name) + + def _np_array_to_pz_dict(self, array: np.ndarray) -> Dict[str, np.ndarray]: + """ + Returns a dictionary containing individual observations for each agent. + Assumes that the batch dimension represents individual agents. + """ + out = {} + for idx, value in enumerate(array): + out[self.possible_agents[idx]] = value + return out + + def _singleton_to_pz_dict(self, value: bool) -> Dict[str, bool]: + """ + Broadcasts the `done` and `trunc` flags to dictionaries keyed by agent id. + """ + return {str(agent_index): value for agent_index in range(self.n_agents)} + + def reset( + self, env_task: int + ) -> Tuple[Dict[AgentID, ObsType], Dict[AgentID, dict]]: + """ + Resets the environment and returns a dictionary of observations + keyed by agent ID. + """ + self.env.seed(env_task) + obs = self.env.reset_random() # random level generation + pz_obs = self._np_array_to_pz_dict(obs["image"]) + + return pz_obs + + def step( + self, action: Dict[AgentID, ActionType], device: str, agent_task: int + ) -> Tuple[ + Dict[AgentID, ObsType], + Dict[AgentID, float], + Dict[AgentID, bool], + Dict[AgentID, bool], + Dict[AgentID, dict], + ]: + """ + Takes inputs in the PettingZoo (PZ) Parallel API format, performs a step and + returns outputs in PZ format. + """ + action = batchify(action, device) + obs, rew, done, info = self.env.step(action) + obs = obs["image"] + trunc = False # there is no `truncated` flag in this environment + self.task_completion = self._task_completion(obs, rew, done, trunc, info) + # convert outputs back to PZ format + obs, rew = map(self._np_array_to_pz_dict, [obs, rew]) + done, trunc, info = map( + self._singleton_to_pz_dict, [done, trunc, self.task_completion] + ) + info["agent_id"] = agent_task + self.n_steps += 1 + + return self.observation(obs), rew, done, trunc, info + + +class Agent(nn.Module): + def __init__(self, input_channels: int, num_actions: int) -> None: + super().__init__() + + self.conv = nn.Sequential( + self.layer_init( + nn.Conv2d(input_channels, out_channels=16, kernel_size=3, stride=1) + ), + nn.ReLU(), + ) + self.lstm = nn.LSTM(input_size=16 * 3 * 3, hidden_size=256, batch_first=True) + self.lstm_init() + + self.mlp = nn.Sequential( + nn.ReLU(), + self.layer_init(nn.Linear(256, 32)), + nn.ReLU(), + ) + # TODO: the paper doesn't mention the critic network? + self.actor = self.layer_init(nn.Linear(32, num_actions), scale=0.01) + self.critic = self.layer_init(nn.Linear(32, 1), scale=1) + + def get_states(self, x, lstm_state, done): + # add batch dim if missing + if len(x.shape) == 3: + x = x.unsqueeze(0) # shape: batch_size, *obs_shape + + hidden = self.conv(x / 255.0) # shape: batch_size, n_features, kernel, kernel + + batch_size = hidden.size(0) + hidden = hidden.reshape(batch_size, -1) # shape: batch_size, features + hidden = hidden.unsqueeze(1) # => shape: batch_size, seq_len=1, n_features + + new_hidden = [] + # reset lstm state if done + for h, d in zip(hidden, done): + h, lstm_state = self.lstm( + h.unsqueeze(0), + ( + torch.logical_not(d).view(1, -1, 1) * lstm_state[0], + torch.logical_not(d).view(1, -1, 1) * lstm_state[1], + ), + ) + new_hidden += [h] + + new_hidden = torch.flatten(torch.cat(new_hidden), 0, 1) + return new_hidden, lstm_state + + def get_value(self, x, lstm_state, done): + hidden, _ = self.get_states(x, lstm_state, done) + hidden = self.mlp(hidden) + return self.critic(hidden) + + def get_action_and_value(self, x, lstm_state, done, action=None): + hidden, lstm_state = self.get_states(x, lstm_state, done) + hidden = self.mlp(hidden) + logits = self.actor(hidden) + probs = Categorical(logits=logits) + + if action is None: + action = probs.sample() + + return ( + action, + probs.log_prob(action), + probs.entropy(), + self.critic(hidden), + lstm_state, + ) + + def layer_init(self, layer, scale=np.sqrt(2), bias_const=0.0): + torch.nn.init.orthogonal_(layer.weight, scale) + torch.nn.init.constant_(layer.bias, bias_const) + return layer + + def lstm_init(self): + for name, param in self.lstm.named_parameters(): + if "bias" in name: + nn.init.constant_(param, 0) + elif "weight" in name: + nn.init.orthogonal_(param, 1.0) + + +agent_curriculums = { + "SP": SelfPlay, + "FSP": FictitiousSelfPlay, + "PFSP": PrioritizedFictitiousSelfPlay, +} +env_curriculums = { + "DR": DomainRandomization, + "PLR": CentralizedPrioritizedLevelReplay, +} + +if __name__ == "__main__": + args = parse_args() + exp_name = f"{args.exp_name}_{args.env_curriculum}_{args.agent_curriculum}" + run_name = f"lasertag__{exp_name}__seed_{args.seed}__{int(time.time())}" + + if args.track: + wandb.init( + project=args.wandb_project_name, + entity=args.wandb_entity, + sync_tensorboard=True, + config=vars(args), + name=run_name, + monitor_gym=True, + save_code=True, + dir=args.logging_dir, + ) + wandb.run.log_code(os.path.join(args.logging_dir)) + + hyperparameters = vars(args) + html_table = "
ParameterValue
{key}{value}
" + for key, value in hyperparameters.items(): + html_table += f"" + wandb.log({"hyperparameters": wandb.Html(html_table)}) + + writer = SummaryWriter( + os.path.join(args.logging_dir, f"{args.logging_dir}/runs/{run_name}") + ) + writer.add_text( + "hyperparameters", + "|param|value|\n|-|-|\n%s" + % ("\n".join([f"|{key}|{value}|" for key, value in vars(args).items()])), + ) + + if ( + not os.path.exists(f"{args.logging_dir}/{run_name}_checkpoints") + and args.save_agent_checkpoints + ): + os.makedirs(f"{args.logging_dir}/{run_name}_checkpoints", exist_ok=True) + + np.random.seed(args.seed) + + """ALGO PARAMS""" + device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + + stack_size = 3 + frame_size = (5, 5) + n_agents = 2 + num_actions = 5 + + agent_curriculum_settings = { + "device": device, + "storage_path": f"{args.agent_curriculum}_agents", + "max_agents": args.max_agents, + "seed": args.seed, + } + + env_task_space = TaskSpace(spaces.Discrete(args.n_env_tasks)) + env_curriculum_settings = { + "DR": {"task_space": env_task_space}, + "PLR": { + "task_space": env_task_space, + "num_steps": args.rollout_length, + "num_processes": 1, # TODO: modify if using vecenvs + "gamma": args.gamma, + "gae_lambda": args.gae_lambda, + "task_sampler_kwargs_dict": {"strategy": "value_l1"}, + }, + } + + """ LEARNER SETUP """ + agent = Agent(input_channels=stack_size, num_actions=num_actions).to(device) + optimizer = optim.Adam(agent.parameters(), lr=args.learning_rate, eps=args.epsilon) + + """ ENV SETUP """ + env = LasertagAdversarial(record_video=False) # 2 agents by default + env = LasertagParallelWrapper(env=env, n_agents=n_agents) + + agent_curriculum = agent_curriculums[args.agent_curriculum]( + agent=agent, **agent_curriculum_settings + ) + env_curriculum = env_curriculums[args.env_curriculum]( + **env_curriculum_settings[args.env_curriculum] + ) + + curriculum = DualCurriculumWrapper( + env=env, + agent_curriculum=agent_curriculum, + env_curriculum=env_curriculum, + ) + # mp_curriculum = make_multiprocessing_curriculum(curriculum) + + """ ALGO LOGIC: EPISODE STORAGE""" + total_episodic_return = 0 + rb_obs = torch.zeros((args.rollout_length, n_agents, stack_size, *frame_size)).to( + device + ) + rb_actions = torch.zeros((args.rollout_length, n_agents)).to(device) + rb_logprobs = torch.zeros((args.rollout_length, n_agents)).to(device) + rb_rewards = torch.zeros((args.rollout_length, n_agents)).to(device) + rb_terms = torch.zeros((args.rollout_length, n_agents)).to(device) + rb_values = torch.zeros((args.rollout_length, n_agents)).to(device) + + agent_tasks, env_tasks, rewards_history = [], [], [] + agent_c_rew, opp_c_rew = 0, 0 + episode, n_learner_wins = 0, 0 + n_updates = 0 + info = {} + + dones = { + "agent_0": 0, + "agent_1": 0, + } + lstm_state = ( + torch.zeros(agent.lstm.num_layers, 1, agent.lstm.hidden_size).to(device), + torch.zeros(agent.lstm.num_layers, 1, agent.lstm.hidden_size).to(device), + ) + lstm_state_opponent = ( + torch.zeros(agent.lstm.num_layers, 1, agent.lstm.hidden_size).to(device), + torch.zeros(agent.lstm.num_layers, 1, agent.lstm.hidden_size).to(device), + ) + + """ TRAINING LOGIC """ + print("training ...") + with tqdm(total=args.total_updates) as pbar: + while n_updates < args.total_updates: + + initial_lstm_state = (lstm_state[0].clone(), lstm_state[1].clone()) + + with torch.no_grad(): + env_task, agent_task = curriculum.sample() + + env_tasks.append(env_task) + agent_tasks.append(agent_task) + + next_obs = env.reset(env_task) + total_episodic_return = 0 + + for step in range(0, args.rollout_length): + joint_obs = batchify(next_obs, device).squeeze() + agent_obs, opponent_obs = joint_obs + + actions, logprobs, _, values, lstm_state = ( + agent.get_action_and_value( + agent_obs, lstm_state, batchify(dones, device) + ) + ) + + opponent = curriculum.get_opponent(info.get("agent_id", 0)).to( + device + ) + opponent_action, _, _, _, lstm_state_opponent = ( + opponent.get_action_and_value( + opponent_obs, lstm_state_opponent, batchify(dones, device) + ) + ) + + joint_actions = torch.tensor((actions, opponent_action)) + next_obs, rewards, dones, truncs, info = env.step( + unbatchify(joint_actions, env.possible_agents), + device, + agent_task, + ) + + opp_reward = rewards["agent_1"] + if opp_reward != 0: + curriculum.update_winrate(info["agent_id"], opp_reward) + if opp_reward == -1: + n_learner_wins += 1 + + rb_obs[step] = batchify(next_obs, device) + rb_rewards[step] = batchify(rewards, device) + rb_terms[step] = batchify(dones, device) + rb_actions[step] = joint_actions + rb_logprobs[step] = logprobs + rb_values[step] = values.flatten() + + total_episodic_return += rb_rewards[step].cpu().numpy() + + agent_c_rew += rewards["agent_0"] + opp_c_rew += rewards["agent_1"] + grid_size = env.level[3]["grid_size_selected"] + walls_percentage = env.level[3]["clutter_rate_selected"] + + if any([dones[a] for a in dones]) or any( + [truncs[a] for a in truncs] + ): + episode += 1 + rewards_history.append(rewards) + env_task, agent_task = curriculum.sample() + env_tasks.append(env_task) + agent_tasks.append(agent_task) + + writer.add_scalar("charts/grid_size", grid_size, episode) + writer.add_scalar( + "charts/walls_percentage", walls_percentage, episode + ) + learner_winrate = n_learner_wins / episode + writer.add_scalar("charts/learner_winrate", learner_winrate) + + next_obs = env.reset(env_task) + + # store learner checkpoints + if ( + args.save_agent_checkpoints + and n_updates % args.checkpoint_frequency == 0 + ): + print(f"saving checkpoint --{n_updates}") + checkpoint_path = ( + f"{args.logging_dir}/{run_name}_checkpoints/" + f"{curriculum.env_curriculum.name}_" + f"{curriculum.agent_curriculum.name}_{n_updates}" + f"_seed_{args.seed}.pkl" + ) + # --- local checkpoint --- + joblib.dump( + agent, + filename=checkpoint_path, + ) + + if args.env_curriculum == "PLR": + with torch.no_grad(): + next_value = agent.get_value( + torch.tensor(next_obs["agent_0"]).to(device), + lstm_state, + batchify(dones, device), + ) + update = { + "update_type": "on_demand", + "metrics": { + "value": values, + "next_value": next_value, + "rew": rewards["agent_0"], + "dones": dones["0"], + "tasks": env_task, + }, + } + # curriculum.update_on_demand(update) + + # gae + with torch.no_grad(): + next_value = agent.get_value( + torch.tensor(next_obs["agent_0"]).to(device), + lstm_state, + batchify(dones, device), + ) + rb_advantages = torch.zeros_like(rb_rewards).to(device) + last_gae_lam = 0 + for t in reversed(range(args.rollout_length - 1)): + if t == args.rollout_length - 1: + next_non_terminal = 1.0 - rb_terms[t + 1] + next_values = next_value + else: + next_non_terminal = 1.0 - rb_terms[t + 1] + next_values = rb_values[t + 1] + delta = ( + rb_rewards[t] + + args.gamma * next_values * next_non_terminal + - rb_values[t] + ) + rb_advantages[t] = last_gae_lam = ( + delta + + args.gamma + * args.gae_lambda + * next_non_terminal + * last_gae_lam + ) + rb_returns = rb_advantages + rb_values + + b_obs = torch.flatten(rb_obs[: args.rollout_length], start_dim=0, end_dim=1) + b_logprobs = torch.flatten( + rb_logprobs[: args.rollout_length], start_dim=0, end_dim=1 + ) + b_actions = torch.flatten( + rb_actions[: args.rollout_length], start_dim=0, end_dim=1 + ) + b_returns = torch.flatten( + rb_returns[: args.rollout_length], start_dim=0, end_dim=1 + ) + b_values = torch.flatten( + rb_values[: args.rollout_length], start_dim=0, end_dim=1 + ) + b_advantages = torch.flatten( + rb_advantages[: args.rollout_length], start_dim=0, end_dim=1 + ) + b_terms = torch.flatten( + rb_terms[: args.rollout_length], start_dim=0, end_dim=1 + ) + + b_index = np.arange(len(b_obs)) + clip_fracs = [] + for repeat in range(args.epochs): + np.random.shuffle(b_index) + for start in range(0, len(b_obs), args.batch_size): + # select the indices we want to train on + end = start + args.batch_size + batch_index = b_index[start:end] + + _, newlogprob, entropy, value, _ = agent.get_action_and_value( + b_obs[batch_index], + ( + initial_lstm_state[0], + initial_lstm_state[1], + ), + b_terms[batch_index], + b_actions.long()[batch_index], + ) + logratio = newlogprob - b_logprobs[batch_index] + ratio = logratio.exp() + + with torch.no_grad(): + # calculate approx_kl http://joschu.net/blog/kl-approx.html + old_approx_kl = (-logratio).mean() + approx_kl = ((ratio - 1) - logratio).mean() + clip_fracs += [ + ((ratio - 1.0).abs() > args.clip_coef).float().mean().item() + ] + + # normalize advantages + rb_advantages = b_advantages[batch_index] + rb_advantages = (rb_advantages - rb_advantages.mean()) / ( + rb_advantages.std() + 1e-8 + ) + + # Policy loss + pg_loss1 = -b_advantages[batch_index] * ratio + pg_loss2 = -b_advantages[batch_index] * torch.clamp( + ratio, 1 - args.clip_coef, 1 + args.clip_coef + ) + pg_loss = torch.max(pg_loss1, pg_loss2).mean() + + # Value loss + value = value.flatten() + v_loss_unclipped = (value - b_returns[batch_index]) ** 2 + v_clipped = b_values[batch_index] + torch.clamp( + value - b_values[batch_index], + -args.clip_coef, + args.clip_coef, + ) + v_loss_clipped = (v_clipped - b_returns[batch_index]) ** 2 + v_loss_max = torch.max(v_loss_unclipped, v_loss_clipped) + v_loss = 0.5 * v_loss_max.mean() + + entropy_loss = entropy.mean() + loss = ( + pg_loss - args.ent_coef * entropy_loss + v_loss * args.vf_coef + ) + + optimizer.zero_grad() + loss.backward() + optimizer.step() + + # writer.add_scalar("losses/value_loss", v_loss.item(), n_updates) + # writer.add_scalar("losses/policy_loss", pg_loss.item(), n_updates) + # writer.add_scalar("losses/entropy", entropy_loss.item(), n_updates) + # writer.add_scalar( + # "losses/old_approx_kl", old_approx_kl.item(), n_updates + # ) + # writer.add_scalar("losses/approx_kl", approx_kl.item(), n_updates) + n_updates += 1 + pbar.update(1) + + # update opponent + if args.agent_curriculum in ["FSP", "PFSP"]: + if ( + n_updates % args.agent_update_frequency == 0 + and episode != 0 + ): + curriculum.update_agent(agent) + + y_pred, y_true = b_values.cpu().numpy(), b_returns.cpu().numpy() + var_y = np.var(y_true) + explained_var = ( + np.nan if var_y == 0 else 1 - np.var(y_true - y_pred) / var_y + ) + + if args.track: + # agent tasks + fig = px.histogram(agent_tasks, height=400) + fig.update_layout(bargap=0.2) + fig.update_layout(showlegend=False) + wandb.log({"charts/agent_tasks": wandb.Html(plotly.io.to_html(fig))}) + + # env tasks + fig = px.histogram(env_tasks, height=400) + fig.update_layout(bargap=0.2) + fig.update_layout(showlegend=False) + wandb.log({"charts/env_tasks": wandb.Html(plotly.io.to_html(fig))}) + + learner_winrate = n_learner_wins / episode + wandb.run.summary["learner_winrate"] = learner_winrate + + # agent rewards + fig = px.line( + pd.DataFrame(rewards_history).cumsum(), + title="Agent rewards", + labels={"index": "Episodes", "value": "Cumulative rewards"}, + ) + wandb.log({"charts/agent_rewards": wandb.Html(plotly.io.to_html(fig))}) + + # win rates and replays + if args.agent_curriculum in ["FSP", "PFSP"]: + agent_ids = np.arange(agent_curriculum_settings["max_agents"]) + values = list(curriculum.agent_curriculum.history.values()) + winrates = [i["winrate"] for i in values] + n_games = [i["n_games"] for i in values] + + fig = make_subplots( + rows=2, cols=1, subplot_titles=("Win Rate", "Number of Games") + ) + fig.add_trace( + go.Bar(x=agent_ids, y=winrates, name="Win Rate", marker_color="blue"), + row=1, + col=1, + ) + fig.add_trace( + go.Bar( + x=agent_ids, + y=n_games, + name="Number of Games", + marker_color="orange", + ), + row=2, + col=1, + ) + + fig.update_yaxes(range=[0, 1], row=1, col=1) + fig.update_layout(showlegend=False) + wandb.log({"charts/opponent_winrates": wandb.Html(plotly.io.to_html(fig))}) + + writer.close() + writer.close() diff --git a/syllabus/examples/experimental/lasertag_round_robin.py b/syllabus/examples/experimental/lasertag_round_robin.py new file mode 100644 index 00000000..33320205 --- /dev/null +++ b/syllabus/examples/experimental/lasertag_round_robin.py @@ -0,0 +1,416 @@ +import argparse +import json +import os +import sys +from dataclasses import dataclass +from typing import Dict, Tuple, TypeVar + +import joblib +import numpy as np +import torch +import torch.nn as nn +from torch.distributions.categorical import Categorical +from tqdm import tqdm + +from syllabus.core import TaskWrapper +from syllabus.examples.experimental.lasertag_dr import batchify, unbatchify + +sys.path.append("../../..") +from lasertag import ( # noqa + LasertagArena1, + LasertagArena2, + LasertagCorridor1, + LasertagCorridor2, + LasertagCross, + LasertagLargeCorridor, + LasertagMaze1, + LasertagMaze2, + LasertagRuins, + LasertagRuins2, + LasertagSixteenRoomsN2, + LasertagStar, +) + +AgentType = TypeVar("AgentType") +AgentID = TypeVar("AgentID") +AgentCurriculum = TypeVar("AgentCurriculum") +EnvCurriculum = TypeVar("EnvCurriculum") +ActionType = TypeVar("ActionType") +ObsType = TypeVar("ObsType") + + +test_envs = { + "LasertagArena1": LasertagArena1, + "LasertagArena2": LasertagArena2, + "LasertagCorridor1": LasertagCorridor1, + "LasertagCorridor2": LasertagCorridor2, + "LasertagMaze1": LasertagMaze1, + "LasertagMaze2": LasertagMaze2, + "LasertagRuins": LasertagRuins, + "LasertagRuins2": LasertagRuins2, + "LasertagStar": LasertagStar, + "LasertagCross": LasertagCross, + "LasertagLargeCorridor": LasertagLargeCorridor, + "LasertagSixteenRoomsN2": LasertagSixteenRoomsN2, +} + + +def parse_args(): + parser = argparse.ArgumentParser() + parser.add_argument( + "--agent-curriculum-1", type=str, default="SP", choices=["SP", "FSP", "PFSP"] + ) + parser.add_argument( + "--env-curriculum-1", type=str, default="DR", choices=["DR", "PLR"] + ) + parser.add_argument( + "--base-path-1", + type=str, + default="", + ) + parser.add_argument( + "--agent-curriculum-2", type=str, default="SP", choices=["SP", "FSP", "PFSP"] + ) + parser.add_argument( + "--env-curriculum-2", type=str, default="DR", choices=["DR", "PLR"] + ) + parser.add_argument( + "--base-path-2", + type=str, + default="", + ) + parser.add_argument("--n-episodes", type=int, default=10) + parser.add_argument( + "--exp-name", + type=str, + default="lasertag_RR", + help="the name of this experiment", + ) + parser.add_argument( + "--wandb-project-name", + type=str, + default="syllabus", + help="the wandb's project name", + ) + parser.add_argument( + "--wandb-entity", + type=str, + default="rpegoud", + help="the entity (team) of wandb's project", + ) + parser.add_argument( + "--logging-dir", + type=str, + default=".", + help="the base directory for logging and wandb storage.", + ) + + args = parser.parse_args() + return args + + +class Agent(nn.Module): + def __init__(self, num_actions): + super().__init__() + + self.network = nn.Sequential( + self._layer_init(nn.Linear(3 * 5 * 5, 512)), + nn.ReLU(), + ) + self.actor = self._layer_init(nn.Linear(512, num_actions), std=0.01) + self.critic = self._layer_init(nn.Linear(512, 1)) + + def _layer_init(self, layer, std=np.sqrt(2), bias_const=0.0): + torch.nn.init.orthogonal_(layer.weight, std) + torch.nn.init.constant_(layer.bias, bias_const) + return layer + + def get_value(self, x, flatten_start_dim=1): + x = torch.flatten(x, start_dim=flatten_start_dim) + return self.critic(self.network(x / 255.0)) + + def get_action_and_value(self, x, action=None, flatten_start_dim=1): + x = torch.flatten(x, start_dim=flatten_start_dim) + hidden = self.network(x / 255.0) + logits = self.actor(hidden) + probs = Categorical(logits=logits) + if action is None: + action = probs.sample() + return action, probs.log_prob(action), probs.entropy(), self.critic(hidden) + + +class LasertagFixedParallelWrapper(TaskWrapper): + """ + Wrapper ensuring compatibility with the PettingZoo Parallel API. + Used with fixed Lasertag environments (deterministic resets for benchmarking). + + Lasertag Environment: + * Action shape: `n_agents` * `Discrete(5)` + * Observation shape: Dict('image': Box(0, 255, (`n_agents`, 3, 5, 5), uint8)) + """ + + def __init__(self, n_agents, *args, **kwargs): + super().__init__(*args, **kwargs) + self.n_agents = n_agents + self.task = None + self.episode_return = 0 + self.possible_agents = [f"agent_{i}" for i in range(self.n_agents)] + self.n_steps = 0 + self.max_steps = self.env.max_steps + + def __getattr__(self, name): + """ + Delegate attribute lookup to the wrapped environment if the attribute + is not found in the LasertagParallelWrapper instance. + """ + return getattr(self.env, name) + + def _np_array_to_pz_dict(self, array: np.ndarray) -> Dict[str, np.ndarray]: + """ + Returns a dictionary containing individual observations for each agent. + Assumes that the batch dimension represents individual agents. + """ + out = {} + for idx, value in enumerate(array): + out[self.possible_agents[idx]] = value + return out + + def _singleton_to_pz_dict(self, value: bool) -> Dict[str, bool]: + """ + Broadcasts the `done` and `trunc` flags to dictionaries keyed by agent id. + """ + return {str(agent_index): value for agent_index in range(self.n_agents)} + + def reset(self) -> Tuple[Dict[AgentID, ObsType], Dict[AgentID, dict]]: + """ + Resets the environment and returns a dictionary of observations + keyed by agent ID. + """ + obs = self.env.reset() # random level generation + pz_obs = self._np_array_to_pz_dict(obs["image"]) + + return pz_obs + + def step(self, action: Dict[AgentID, ActionType], device: str) -> Tuple[ + Dict[AgentID, ObsType], + Dict[AgentID, float], + Dict[AgentID, bool], + Dict[AgentID, bool], + Dict[AgentID, dict], + ]: + """ + Takes inputs in the PettingZoo (PZ) Parallel API format, performs a step and + returns outputs in PZ format. + """ + action = batchify(action, device) + obs, rew, done, info = self.env.step(action) + obs = obs["image"] + trunc = False # there is no `truncated` flag in this environment + self.task_completion = self._task_completion(obs, rew, done, trunc, info) + # convert outputs back to PZ format + obs, rew = map(self._np_array_to_pz_dict, [obs, rew]) + done, trunc, info = map( + self._singleton_to_pz_dict, [done, trunc, self.task_completion] + ) + self.n_steps += 1 + + return self.observation(obs), rew, done, trunc, info + + +@dataclass +class AgentConfig: + """ + Dataclass used to store information about the current version + of an agent, as defined by its checkpoint, seed and curriculums. + """ + + agent_curriculum: str + env_curriculum: str + base_path: str + agent = None + checkpoint: int = None + seed: int = None + + def __post_init__(self): + self.get_checkpoints_and_seeds() + + def set_task(self, checkpoint: int, seed: int) -> None: + self.checkpoint = checkpoint + self.seed = seed + + @property + def path(self) -> str: + return f"{self.env_curriculum}_{self.agent_curriculum}" + + def get_checkpoints_and_seeds(self) -> None: + """Extracts all unique checkpoints and seeds from a checkpoint folder path.""" + files = os.listdir(f"{self.base_path}") + checkpoints = [file for file in files if file != "cached"] + self.checkpoints = set(map(lambda x: x.split("_")[2], checkpoints)) + self.seeds = set(map(lambda x: x.split("_")[-1].split(".")[0], checkpoints)) + + def __str__(self) -> str: + return ( + f"{self.env_curriculum}_{self.agent_curriculum}_" + f"{self.checkpoint}_seed_{self.seed}" + ) + + +def load_agent( + agent_cfg: AgentConfig, + device: str = "cpu", +) -> Tuple[AgentType, AgentConfig]: + """Loads an agent to `device` and updates `AgentConfig`""" + path = f"{agent_cfg.base_path}/{str(agent_cfg)}.pkl" + try: + agent = joblib.load(path) + agent = agent.to(device) + + agent_cfg.agent = agent + except FileNotFoundError: + return None, agent_cfg + + return agent, agent_cfg + + +def play_n_episodes( + agent_1_cfg: AgentConfig, + agent_2_cfg: AgentConfig, + device: str, + n_episodes: int = 10, + environment_id: str = "LasertagArena1", +) -> Tuple[float, float]: + + n_agents = 2 + stack_size = 3 + env = test_envs[environment_id]() # 2 agents by default + env = LasertagFixedParallelWrapper(env=env, n_agents=n_agents) + + frame_size = (env.agent_view_size, env.agent_view_size) + max_cycles = env.max_steps + agent_1_c_rew, agent_2_c_rew = 0, 0 + + """ALGO LOGIC: EPISODE STORAGE""" + rb_obs = torch.zeros((max_cycles, n_agents, stack_size, *frame_size)).to(device) + rb_actions = torch.zeros((max_cycles, n_agents)).to(device) + rb_rewards = torch.zeros((max_cycles, n_agents)).to(device) + rb_terms = torch.zeros((max_cycles, n_agents)).to(device) + + agent_1, agent_2 = agent_1_cfg.agent, agent_2_cfg.agent + + dones = { + "agent_0": 0, + "agent_1": 0, + } + lstm_state_1 = ( + torch.zeros(agent_1.lstm.num_layers, 1, agent_1.lstm.hidden_size).to(device), + torch.zeros(agent_1.lstm.num_layers, 1, agent_1.lstm.hidden_size).to(device), + ) + lstm_state_2 = ( + torch.zeros(agent_2.lstm.num_layers, 1, agent_2.lstm.hidden_size).to(device), + torch.zeros(agent_2.lstm.num_layers, 1, agent_2.lstm.hidden_size).to(device), + ) + + """ TRAINING LOGIC """ + for episode in range(n_episodes): + # collect an episode + with torch.no_grad(): + + next_obs = env.reset() + + # each episode has num_steps + for step in range(0, max_cycles): + # rollover the observation + joint_obs = batchify(next_obs, device).squeeze() + agent_obs, opponent_obs = joint_obs.split(1, dim=0) + + agent_obs = agent_obs.squeeze().to(device) + opponent_obs = opponent_obs.squeeze().to(device) + + # get action from the agent and the opponent + agent_1_action, _, _, _, lstm_state_1 = agent_1.get_action_and_value( + agent_obs, lstm_state_1, batchify(dones, device) + ) + + agent_2_action, _, _, _, lstm_state_2 = agent_1.get_action_and_value( + agent_obs, lstm_state_2, batchify(dones, device) + ) + + # execute the environment and log data + joint_actions = torch.tensor((agent_1_action, agent_2_action)) + next_obs, rewards, dones, truncs, _ = env.step( + unbatchify(joint_actions, env.possible_agents), device + ) + + agent_1_c_rew += rewards["agent_0"] + agent_2_c_rew += rewards["agent_1"] + + # add to episode storage + rb_obs[step] = batchify(next_obs, device) + rb_rewards[step] = batchify(rewards, device) + rb_terms[step] = batchify(dones, device) + rb_actions[step] = joint_actions + + # if we reach termination or truncation, end + if any([dones[a] for a in dones]) or any([truncs[a] for a in truncs]): + break + + agent_1_norm_rew = agent_1_c_rew / n_episodes + agent_2_norm_rew = agent_2_c_rew / n_episodes + + return agent_1_norm_rew, agent_2_norm_rew + + +if __name__ == "__main__": + args = parse_args() + agent_1_cfg = AgentConfig( + args.agent_curriculum_1, args.env_curriculum_1, args.base_path_1 + ) + agent_2_cfg = AgentConfig( + args.agent_curriculum_2, args.env_curriculum_2, args.base_path_2 + ) + device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + + if not os.path.exists(f"{args.logging_dir}"): + os.makedirs(f"{args.logging_dir}", exist_ok=True) + + logs = { + agent.path: {checkpoint: [] for checkpoint in agent.checkpoints} + for agent in (agent_1_cfg, agent_2_cfg) + } + + for checkpoint_1 in tqdm(agent_1_cfg.checkpoints, desc="outer loop"): + for seed_1 in agent_1_cfg.seeds: + agent_1_cfg.set_task(checkpoint_1, seed_1) + agent_1, agent_1_cfg = load_agent(agent_1_cfg, device) + + for checkpoint_2 in tqdm(agent_2_cfg.checkpoints, desc="inner loop"): + for seed_2 in agent_2_cfg.seeds: + agent_2_cfg.set_task(checkpoint_2, seed_2) + agent_2, agent_2_cfg = load_agent(agent_2_cfg, device) + + if agent_1 is None or agent_2 is None: + print( + f"Skipping checkpoints=[{checkpoint_1},{checkpoint_2}] seeds=[{seed_1},{seed_2}]" + ) + continue + + for environment_id in list(test_envs.keys()): + returns_1, returns_2 = play_n_episodes( + agent_1_cfg, + agent_2_cfg, + device, + n_episodes=args.n_episodes, + environment_id=environment_id, + ) + logs[agent_1_cfg.path][checkpoint_1].append(returns_1) + logs[agent_2_cfg.path][checkpoint_2].append(returns_2) + + if not os.path.exists(f"{args.logging_dir}/round_robin/"): + os.makedirs(f"{args.logging_dir}/round_robin/") + + with open( + f"{args.logging_dir}/round_robin/{agent_1_cfg.path}_{agent_2_cfg.path}.json", + "w", + ) as outfile: + json.dump(logs, outfile) diff --git a/syllabus/examples/experimental/lasertag_vecenv_test.ipynb b/syllabus/examples/experimental/lasertag_vecenv_test.ipynb new file mode 100644 index 00000000..2ab07149 --- /dev/null +++ b/syllabus/examples/experimental/lasertag_vecenv_test.ipynb @@ -0,0 +1,270 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# ***Lasertag Vecenv***" + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "metadata": {}, + "outputs": [], + "source": [ + "import argparse\n", + "import os\n", + "import sys\n", + "import time\n", + "from typing import Dict, Tuple, TypeVar\n", + "\n", + "import joblib\n", + "import numpy as np\n", + "import pandas as pd\n", + "import plotly\n", + "import plotly.express as px\n", + "import plotly.graph_objects as go\n", + "import torch\n", + "import torch.nn as nn\n", + "import torch.optim as optim\n", + "import wandb\n", + "from gymnasium import spaces\n", + "from plotly.subplots import make_subplots\n", + "from torch.distributions.categorical import Categorical\n", + "from torch.utils.tensorboard import SummaryWriter\n", + "from tqdm.auto import tqdm\n", + "import supersuit as ss\n", + "sys.path.append(\"../../..\")\n", + "from lasertag import LasertagAdversarial # noqa: E402\n", + "from syllabus.core import ( # noqa: E402\n", + " DualCurriculumWrapper,\n", + " TaskWrapper,\n", + " make_multiprocessing_curriculum,\n", + ")\n", + "\n", + "# noqa: E402\n", + "from syllabus.curricula import ( # noqa: E402\n", + " CentralizedPrioritizedLevelReplay,\n", + " DomainRandomization,\n", + " FictitiousSelfPlay,\n", + " PrioritizedFictitiousSelfPlay,\n", + " SelfPlay,\n", + ")\n", + "from syllabus.task_space import TaskSpace # noqa: E402\n", + "\n", + "ActionType = TypeVar(\"ActionType\")\n", + "AgentID = TypeVar(\"AgentID\")\n", + "AgentType = TypeVar(\"AgentType\")\n", + "EnvTask = TypeVar(\"EnvTask\")\n", + "AgentTask = TypeVar(\"AgentTask\")\n", + "ObsType = TypeVar(\"ObsType\")" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "metadata": {}, + "outputs": [], + "source": [ + "from pettingzoo import ParallelEnv" + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "metadata": {}, + "outputs": [], + "source": [ + "def batchify(x, device):\n", + " \"\"\"Converts PZ style returns to batch of torch arrays.\"\"\"\n", + " # convert to list of np arrays\n", + " x = np.stack([x[a] for a in x], axis=0)\n", + " # convert to torch\n", + " x = torch.tensor(x).to(device)\n", + "\n", + " return x\n", + "\n", + "\n", + "def unbatchify(x, possible_agents: np.ndarray):\n", + " \"\"\"Converts np array to PZ style arguments.\"\"\"\n", + " x = x.cpu().numpy()\n", + " x = {agent: x[idx] for idx, agent in enumerate(possible_agents)}\n", + "\n", + " return x\n", + "\n", + "\n", + "class LasertagParallelWrapper(ParallelEnv, TaskWrapper):\n", + " \"\"\"\n", + " Wrapper ensuring compatibility with the PettingZoo Parallel API.\n", + "\n", + " Lasertag Environment:\n", + " * Action shape: `n_agents` * `Discrete(5)`\n", + " * Observation shape: Dict('image': Box(0, 255, (`n_agents`, 3, 5, 5), uint8))\n", + " \"\"\"\n", + "\n", + " def __init__(self, n_agents, *args, **kwargs):\n", + " super().__init__(*args, **kwargs)\n", + " self.n_agents = n_agents\n", + " self.task = None\n", + " self.episode_return = 0\n", + " self.possible_agents = [f\"agent_{i}\" for i in range(self.n_agents)]\n", + " self.n_steps = 0\n", + " self.observation_spaces = {\n", + " f\"{agent}\": spaces.Box(\n", + " low=0,\n", + " high=255,\n", + " shape=(\n", + " self.n_agents,\n", + " 3,\n", + " self.agent_view_size,\n", + " self.agent_view_size,\n", + " ),\n", + " dtype=\"uint8\",\n", + " )\n", + " for agent in self.possible_agents\n", + " }\n", + " self.action_spaces = {\n", + " f\"{agent}\": spaces.Discrete(5) for agent in self.possible_agents\n", + " }\n", + "\n", + " def __getattr__(self, name):\n", + " \"\"\"\n", + " Delegate attribute lookup to the wrapped environment if the attribute\n", + " is not found in the LasertagParallelWrapper instance.\n", + " \"\"\"\n", + " return getattr(self.env, name)\n", + "\n", + " def _np_array_to_pz_dict(self, array: np.ndarray) -> Dict[str, np.ndarray]:\n", + " \"\"\"\n", + " Returns a dictionary containing individual observations for each agent.\n", + " Assumes that the batch dimension represents individual agents.\n", + " \"\"\"\n", + " out = {}\n", + " for idx, value in enumerate(array):\n", + " out[self.possible_agents[idx]] = value\n", + " return out\n", + "\n", + " def _singleton_to_pz_dict(self, value: bool) -> Dict[str, bool]:\n", + " \"\"\"\n", + " Broadcasts the `done` and `trunc` flags to dictionaries keyed by agent id.\n", + " \"\"\"\n", + " return {str(agent_index): value for agent_index in range(self.n_agents)}\n", + "\n", + " def reset(\n", + " self, env_task: int\n", + " ) -> Tuple[Dict[AgentID, ObsType], Dict[AgentID, dict]]:\n", + " \"\"\"\n", + " Resets the environment and returns a dictionary of observations\n", + " keyed by agent ID.\n", + " \"\"\"\n", + " self.env.seed(env_task)\n", + " obs = self.env.reset_random() # random level generation\n", + " pz_obs = self._np_array_to_pz_dict(obs[\"image\"])\n", + "\n", + " return pz_obs\n", + "\n", + " def step(\n", + " self, action: Dict[AgentID, ActionType], device: str, agent_task: int\n", + " ) -> Tuple[\n", + " Dict[AgentID, ObsType],\n", + " Dict[AgentID, float],\n", + " Dict[AgentID, bool],\n", + " Dict[AgentID, bool],\n", + " Dict[AgentID, dict],\n", + " ]:\n", + " \"\"\"\n", + " Takes inputs in the PettingZoo (PZ) Parallel API format, performs a step and\n", + " returns outputs in PZ format.\n", + " \"\"\"\n", + " action = batchify(action, device)\n", + " obs, rew, done, info = self.env.step(action)\n", + " obs = obs[\"image\"]\n", + " trunc = False # there is no `truncated` flag in this environment\n", + " self.task_completion = self._task_completion(obs, rew, done, trunc, info)\n", + " # convert outputs back to PZ format\n", + " obs, rew = map(self._np_array_to_pz_dict, [obs, rew])\n", + " done, trunc, info = map(\n", + " self._singleton_to_pz_dict, [done, trunc, self.task_completion]\n", + " )\n", + " info[\"agent_id\"] = agent_task\n", + " self.n_steps += 1\n", + "\n", + " return self.observation(obs), rew, done, trunc, info" + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "metadata": {}, + "outputs": [], + "source": [ + "env = LasertagAdversarial(record_video=False) # 2 agents by default\n", + "env = LasertagParallelWrapper(env=env, n_agents=2)" + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "c:\\Users\\ryanp\\AppData\\Local\\Programs\\Python\\Python310\\lib\\site-packages\\pettingzoo\\utils\\env.py:358: UserWarning: Your environment should override the observation_space function. Attempting to use the observation_spaces dict attribute.\n", + " warnings.warn(\n", + "c:\\Users\\ryanp\\AppData\\Local\\Programs\\Python\\Python310\\lib\\site-packages\\pettingzoo\\utils\\env.py:370: UserWarning: Your environment should override the action_space function. Attempting to use the action_spaces dict attribute.\n", + " warnings.warn(\n" + ] + }, + { + "ename": "TypeError", + "evalue": "cannot pickle 'python_griddly.GDY' object", + "output_type": "error", + "traceback": [ + "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[1;31mTypeError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[1;32mIn[39], line 4\u001b[0m\n\u001b[0;32m 1\u001b[0m num_envs \u001b[38;5;241m=\u001b[39m \u001b[38;5;241m2\u001b[39m\n\u001b[0;32m 3\u001b[0m env \u001b[38;5;241m=\u001b[39m ss\u001b[38;5;241m.\u001b[39mpettingzoo_env_to_vec_env_v1(env)\n\u001b[1;32m----> 4\u001b[0m envs \u001b[38;5;241m=\u001b[39m \u001b[43mss\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mconcat_vec_envs_v1\u001b[49m\u001b[43m(\u001b[49m\n\u001b[0;32m 5\u001b[0m \u001b[43m \u001b[49m\u001b[43menv\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mnum_envs\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m/\u001b[39;49m\u001b[38;5;241;43m/\u001b[39;49m\u001b[43m \u001b[49m\u001b[38;5;241;43m2\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mnum_cpus\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;241;43m0\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mbase_class\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mgymnasium\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\n\u001b[0;32m 6\u001b[0m \u001b[43m)\u001b[49m\n", + "File \u001b[1;32mc:\\Users\\ryanp\\AppData\\Local\\Programs\\Python\\Python310\\lib\\site-packages\\supersuit\\vector\\vector_constructors.py:64\u001b[0m, in \u001b[0;36mconcat_vec_envs_v1\u001b[1;34m(vec_env, num_vec_envs, num_cpus, base_class)\u001b[0m\n\u001b[0;32m 62\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mconcat_vec_envs_v1\u001b[39m(vec_env, num_vec_envs, num_cpus\u001b[38;5;241m=\u001b[39m\u001b[38;5;241m0\u001b[39m, base_class\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mgymnasium\u001b[39m\u001b[38;5;124m\"\u001b[39m):\n\u001b[0;32m 63\u001b[0m num_cpus \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mmin\u001b[39m(num_cpus, num_vec_envs)\n\u001b[1;32m---> 64\u001b[0m vec_env \u001b[38;5;241m=\u001b[39m \u001b[43mMakeCPUAsyncConstructor\u001b[49m\u001b[43m(\u001b[49m\u001b[43mnum_cpus\u001b[49m\u001b[43m)\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mvec_env_args\u001b[49m\u001b[43m(\u001b[49m\u001b[43mvec_env\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mnum_vec_envs\u001b[49m\u001b[43m)\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 66\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m base_class \u001b[38;5;241m==\u001b[39m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mgymnasium\u001b[39m\u001b[38;5;124m\"\u001b[39m:\n\u001b[0;32m 67\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m vec_env\n", + "File \u001b[1;32mc:\\Users\\ryanp\\AppData\\Local\\Programs\\Python\\Python310\\lib\\site-packages\\supersuit\\vector\\concat_vec_env.py:23\u001b[0m, in \u001b[0;36mConcatVecEnv.__init__\u001b[1;34m(self, vec_env_fns, obs_space, act_space)\u001b[0m\n\u001b[0;32m 22\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21m__init__\u001b[39m(\u001b[38;5;28mself\u001b[39m, vec_env_fns, obs_space\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mNone\u001b[39;00m, act_space\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mNone\u001b[39;00m):\n\u001b[1;32m---> 23\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mvec_envs \u001b[38;5;241m=\u001b[39m vec_envs \u001b[38;5;241m=\u001b[39m [vec_env_fn() \u001b[38;5;28;01mfor\u001b[39;00m vec_env_fn \u001b[38;5;129;01min\u001b[39;00m vec_env_fns]\n\u001b[0;32m 24\u001b[0m \u001b[38;5;28;01mfor\u001b[39;00m i \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28mrange\u001b[39m(\u001b[38;5;28mlen\u001b[39m(vec_envs)):\n\u001b[0;32m 25\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28mhasattr\u001b[39m(vec_envs[i], \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mnum_envs\u001b[39m\u001b[38;5;124m\"\u001b[39m):\n", + "File \u001b[1;32mc:\\Users\\ryanp\\AppData\\Local\\Programs\\Python\\Python310\\lib\\site-packages\\supersuit\\vector\\concat_vec_env.py:23\u001b[0m, in \u001b[0;36m\u001b[1;34m(.0)\u001b[0m\n\u001b[0;32m 22\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21m__init__\u001b[39m(\u001b[38;5;28mself\u001b[39m, vec_env_fns, obs_space\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mNone\u001b[39;00m, act_space\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mNone\u001b[39;00m):\n\u001b[1;32m---> 23\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mvec_envs \u001b[38;5;241m=\u001b[39m vec_envs \u001b[38;5;241m=\u001b[39m [\u001b[43mvec_env_fn\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m \u001b[38;5;28;01mfor\u001b[39;00m vec_env_fn \u001b[38;5;129;01min\u001b[39;00m vec_env_fns]\n\u001b[0;32m 24\u001b[0m \u001b[38;5;28;01mfor\u001b[39;00m i \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28mrange\u001b[39m(\u001b[38;5;28mlen\u001b[39m(vec_envs)):\n\u001b[0;32m 25\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28mhasattr\u001b[39m(vec_envs[i], \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mnum_envs\u001b[39m\u001b[38;5;124m\"\u001b[39m):\n", + "File \u001b[1;32mc:\\Users\\ryanp\\AppData\\Local\\Programs\\Python\\Python310\\lib\\site-packages\\supersuit\\vector\\vector_constructors.py:12\u001b[0m, in \u001b[0;36mvec_env_args..env_fn\u001b[1;34m()\u001b[0m\n\u001b[0;32m 11\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21menv_fn\u001b[39m():\n\u001b[1;32m---> 12\u001b[0m env_copy \u001b[38;5;241m=\u001b[39m cloudpickle\u001b[38;5;241m.\u001b[39mloads(\u001b[43mcloudpickle\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mdumps\u001b[49m\u001b[43m(\u001b[49m\u001b[43menv\u001b[49m\u001b[43m)\u001b[49m)\n\u001b[0;32m 13\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m env_copy\n", + "File \u001b[1;32mc:\\Users\\ryanp\\AppData\\Local\\Programs\\Python\\Python310\\lib\\site-packages\\cloudpickle\\cloudpickle.py:1479\u001b[0m, in \u001b[0;36mdumps\u001b[1;34m(obj, protocol, buffer_callback)\u001b[0m\n\u001b[0;32m 1477\u001b[0m \u001b[38;5;28;01mwith\u001b[39;00m io\u001b[38;5;241m.\u001b[39mBytesIO() \u001b[38;5;28;01mas\u001b[39;00m file:\n\u001b[0;32m 1478\u001b[0m cp \u001b[38;5;241m=\u001b[39m Pickler(file, protocol\u001b[38;5;241m=\u001b[39mprotocol, buffer_callback\u001b[38;5;241m=\u001b[39mbuffer_callback)\n\u001b[1;32m-> 1479\u001b[0m \u001b[43mcp\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mdump\u001b[49m\u001b[43m(\u001b[49m\u001b[43mobj\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 1480\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m file\u001b[38;5;241m.\u001b[39mgetvalue()\n", + "File \u001b[1;32mc:\\Users\\ryanp\\AppData\\Local\\Programs\\Python\\Python310\\lib\\site-packages\\cloudpickle\\cloudpickle.py:1245\u001b[0m, in \u001b[0;36mPickler.dump\u001b[1;34m(self, obj)\u001b[0m\n\u001b[0;32m 1243\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mdump\u001b[39m(\u001b[38;5;28mself\u001b[39m, obj):\n\u001b[0;32m 1244\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[1;32m-> 1245\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43msuper\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mdump\u001b[49m\u001b[43m(\u001b[49m\u001b[43mobj\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 1246\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mRuntimeError\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m e:\n\u001b[0;32m 1247\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mlen\u001b[39m(e\u001b[38;5;241m.\u001b[39margs) \u001b[38;5;241m>\u001b[39m \u001b[38;5;241m0\u001b[39m \u001b[38;5;129;01mand\u001b[39;00m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mrecursion\u001b[39m\u001b[38;5;124m\"\u001b[39m \u001b[38;5;129;01min\u001b[39;00m e\u001b[38;5;241m.\u001b[39margs[\u001b[38;5;241m0\u001b[39m]:\n", + "\u001b[1;31mTypeError\u001b[0m: cannot pickle 'python_griddly.GDY' object" + ] + } + ], + "source": [ + "num_envs = 2\n", + "\n", + "env = ss.pettingzoo_env_to_vec_env_v1(env)\n", + "envs = ss.concat_vec_envs_v1(\n", + " env, num_envs // 2, num_cpus=0, base_class=\"gymnasium\"\n", + ")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.7" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/syllabus/examples/experimental/round_robin/parse_results/DR_FSP_DR_PFSP_returns.json b/syllabus/examples/experimental/round_robin/parse_results/DR_FSP_DR_PFSP_returns.json new file mode 100644 index 00000000..07f8c95c --- /dev/null +++ b/syllabus/examples/experimental/round_robin/parse_results/DR_FSP_DR_PFSP_returns.json @@ -0,0 +1,3058 @@ +{ + "DR_FSP": { + "0": [ + -0.1, + 0.1, + 0.0, + 0.2, + 0.0, + 0.0, + -0.1, + -0.1, + 0.0, + 0.1, + 0.0, + 0.0 + ], + "1000000": [ + -0.2, + 0.1, + -0.3, + -0.1, + 0.0, + 0.0, + 0.1, + 0.1, + 0.0, + 0.0, + 0.1, + 0.0 + ], + "1040000": [ + 0.0, + -0.2, + 0.1, + -0.7, + 0.0, + 0.0, + -0.1, + -0.2, + 0.0, + -0.1, + -0.1, + 0.0 + ], + "1080000": [ + 0.1, + -0.2, + 0.0, + 0.3, + 0.0, + 0.0, + -0.2, + -0.1, + 0.0, + -0.1, + 0.1, + 0.0 + ], + "1120000": [ + -0.1, + 0.3, + 0.3, + 0.1, + 0.0, + 0.0, + -0.1, + 0.0, + 0.0, + 0.2, + 0.0, + 0.0 + ], + "1160000": [ + -0.4, + -0.2, + -0.2, + -0.1, + 0.0, + 0.0, + 0.1, + 0.1, + 0.0, + -0.1, + 0.1, + 0.0 + ], + "1200000": [ + 0.3, + -0.3, + -0.3, + -0.3, + 0.0, + 0.0, + 0.2, + 0.0, + 0.0, + -0.1, + -0.1, + 0.0 + ], + "120000": [ + -0.1, + -0.2, + -0.1, + -0.1, + 0.0, + 0.0, + 0.1, + 0.0, + 0.0, + -0.1, + 0.0, + 0.0 + ], + "1240000": [ + 0.1, + 0.1, + 0.1, + 0.6, + 0.0, + 0.0, + 0.0, + 0.1, + 0.0, + 0.0, + 0.1, + 0.0 + ], + "1280000": [ + 0.0, + 0.0, + 0.1, + 0.1, + 0.0, + 0.0, + -0.3, + 0.0, + 0.0, + -0.2, + 0.0, + 0.0 + ], + "1320000": [ + -0.2, + -0.1, + 0.0, + -0.2, + 0.0, + 0.0, + -0.2, + -0.1, + 0.0, + 0.0, + 0.0, + 0.1 + ], + "1360000": [ + -0.1, + 0.0, + -0.1, + 0.3, + 0.0, + 0.0, + -0.1, + 0.0, + 0.0, + -0.2, + 0.0, + 0.0 + ], + "1400000": [ + 0.1, + 0.0, + -0.2, + -0.2, + 0.0, + 0.0, + 0.1, + -0.1, + 0.0, + 0.1, + 0.0, + 0.0 + ], + "1440000": [ + -0.3, + 0.4, + -0.3, + -0.1, + 0.0, + 0.0, + 0.0, + -0.2, + 0.0, + -0.4, + 0.0, + 0.0 + ], + "1480000": [ + 0.1, + -0.1, + -0.1, + 0.5, + 0.0, + 0.0, + 0.0, + -0.1, + 0.0, + 0.1, + 0.0, + 0.0 + ], + "1520000": [ + -0.2, + 0.1, + -0.4, + 0.5, + 0.0, + 0.0, + 0.0, + -0.1, + 0.0, + 0.2, + 0.0, + 0.0 + ], + "1560000": [ + -0.1, + 0.1, + -0.3, + -0.4, + 0.0, + 0.0, + -0.1, + 0.2, + 0.0, + -0.1, + 0.0, + 0.1 + ], + "1600000": [ + -0.2, + 0.1, + 0.1, + -0.1, + 0.0, + 0.0, + 0.1, + 0.0, + 0.0, + 0.0, + 0.1, + 0.0 + ], + "160000": [ + 0.1, + 0.1, + 0.2, + 0.0, + 0.0, + 0.0, + 0.0, + -0.1, + 0.0, + 0.3, + 0.0, + 0.0 + ], + "1640000": [ + 0.0, + -0.1, + 0.1, + 0.1, + 0.0, + 0.0, + -0.1, + -0.2, + 0.0, + -0.2, + 0.1, + 0.0 + ], + "1680000": [ + -0.2, + 0.0, + 0.2, + -0.2, + 0.0, + 0.0, + 0.3, + 0.0, + 0.0, + -0.1, + 0.0, + 0.0 + ], + "1720000": [ + 0.0, + 0.0, + 0.1, + 0.1, + 0.0, + 0.0, + -0.1, + 0.1, + 0.0, + -0.2, + 0.0, + -0.1 + ], + "1760000": [ + -0.1, + 0.2, + 0.4, + 0.4, + 0.0, + 0.0, + -0.1, + -0.1, + 0.0, + 0.1, + 0.0, + 0.1 + ], + "1800000": [ + -0.2, + 0.0, + -0.1, + 0.3, + 0.0, + 0.0, + 0.4, + -0.2, + 0.0, + 0.1, + 0.1, + 0.0 + ], + "1840000": [ + 0.0, + 0.3, + -0.1, + -0.1, + 0.0, + 0.0, + -0.1, + 0.1, + 0.0, + 0.2, + 0.1, + 0.0 + ], + "1880000": [ + -0.1, + 0.2, + 0.2, + 0.1, + 0.0, + 0.0, + -0.2, + -0.1, + 0.0, + 0.1, + 0.0, + 0.0 + ], + "1920000": [ + -0.3, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.2, + -0.2, + 0.0, + 0.0, + 0.1, + 0.0 + ], + "1960000": [ + -0.1, + 0.4, + 0.1, + 0.2, + 0.0, + 0.0, + -0.1, + -0.1, + 0.0, + 0.2, + -0.3, + 0.0 + ], + "2000000": [ + -0.1, + 0.1, + 0.1, + 0.1, + 0.0, + 0.0, + -0.2, + 0.0, + 0.0, + -0.1, + 0.0, + 0.0 + ], + "200000": [ + 0.0, + 0.2, + 0.0, + 0.1, + 0.0, + 0.0, + -0.1, + -0.1, + 0.0, + 0.2, + 0.0, + 0.0 + ], + "2040000": [ + -0.1, + 0.0, + 0.4, + 0.4, + 0.0, + 0.0, + 0.2, + 0.2, + 0.0, + 0.1, + -0.1, + 0.0 + ], + "2080000": [ + 0.1, + 0.0, + 0.3, + 0.2, + 0.0, + 0.0, + -0.1, + 0.1, + 0.0, + 0.2, + -0.1, + 0.0 + ], + "2120000": [ + 0.1, + 0.1, + 0.1, + 0.4, + 0.0, + 0.0, + 0.0, + 0.1, + 0.0, + 0.4, + 0.2, + 0.0 + ], + "2160000": [ + -0.1, + -0.3, + -0.3, + 0.2, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + -0.1, + 0.0, + 0.0 + ], + "2200000": [ + -0.1, + 0.1, + 0.3, + 0.1, + 0.0, + 0.0, + 0.0, + 0.2, + 0.0, + -0.2, + 0.0, + 0.0 + ], + "2240000": [ + 0.3, + -0.3, + 0.1, + -0.2, + 0.0, + 0.0, + -0.1, + 0.1, + 0.0, + 0.2, + 0.0, + 0.0 + ], + "2280000": [ + 0.0, + 0.0, + -0.2, + 0.0, + 0.0, + 0.0, + 0.3, + 0.2, + 0.0, + 0.0, + -0.1, + 0.0 + ], + "2320000": [ + 0.0, + 0.3, + -0.1, + 0.5, + 0.0, + 0.0, + -0.1, + -0.2, + 0.0, + -0.2, + 0.1, + -0.1 + ], + "2360000": [ + 0.1, + -0.1, + -0.1, + -0.1, + 0.0, + 0.0, + -0.3, + 0.0, + 0.0, + -0.1, + 0.0, + 0.0 + ], + "2400000": [ + -0.3, + 0.0, + 0.1, + 0.1, + 0.0, + 0.0, + 0.1, + 0.1, + 0.0, + 0.1, + 0.1, + 0.1 + ], + "240000": [ + 0.0, + 0.2, + 0.1, + 0.4, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "2440000": [ + -0.2, + 0.0, + 0.1, + 0.1, + 0.0, + 0.0, + -0.1, + 0.3, + 0.0, + 0.0, + -0.1, + 0.0 + ], + "2480000": [ + -0.1, + -0.2, + -0.1, + 0.4, + 0.0, + 0.0, + 0.0, + 0.2, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "2520000": [ + 0.1, + -0.2, + -0.2, + 0.1, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.1, + 0.0, + 0.0 + ], + "2560000": [ + 0.0, + 0.2, + 0.2, + -0.2, + 0.0, + 0.0, + 0.2, + 0.0, + -0.1, + -0.2, + 0.0, + 0.0 + ], + "2600000": [ + 0.0, + -0.2, + 0.0, + 0.0, + 0.0, + 0.0, + -0.2, + 0.3, + 0.0, + 0.4, + -0.2, + -0.1 + ], + "2640000": [ + 0.0, + 0.2, + -0.1, + 0.0, + 0.0, + 0.0, + 0.1, + -0.2, + 0.0, + 0.1, + 0.1, + 0.0 + ], + "2680000": [ + -0.2, + -0.1, + 0.2, + -0.5, + 0.0, + 0.0, + -0.2, + 0.0, + 0.0, + -0.2, + 0.1, + 0.0 + ], + "2720000": [ + 0.1, + 0.0, + 0.2, + 0.2, + 0.0, + 0.0, + 0.1, + 0.1, + 0.0, + 0.2, + -0.1, + 0.0 + ], + "2760000": [ + 0.0, + -0.1, + -0.2, + -0.2, + 0.0, + 0.0, + 0.0, + 0.1, + 0.0, + 0.1, + 0.1, + -0.1 + ], + "2800000": [ + 0.1, + -0.1, + -0.1, + 0.4, + 0.0, + 0.0, + -0.2, + -0.1, + 0.0, + -0.2, + 0.0, + 0.0 + ], + "280000": [ + 0.2, + -0.3, + 0.0, + 0.1, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + -0.1, + 0.0 + ], + "2840000": [ + -0.3, + -0.1, + -0.3, + 0.3, + 0.0, + 0.0, + 0.0, + 0.4, + 0.0, + -0.2, + 0.0, + 0.0 + ], + "2880000": [ + 0.1, + -0.1, + 0.0, + 0.2, + 0.0, + 0.0, + -0.1, + 0.0, + 0.0, + 0.1, + 0.0, + -0.1 + ], + "2920000": [ + 0.0, + -0.1, + 0.0, + -0.1, + 0.0, + 0.0, + -0.1, + 0.0, + 0.0, + -0.2, + -0.3, + 0.0 + ], + "2960000": [ + -0.4, + 0.0, + 0.1, + -0.6, + 0.0, + 0.0, + 0.1, + 0.3, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "3000000": [ + -0.2, + 0.1, + -0.3, + 0.1, + 0.0, + 0.0, + 0.4, + -0.2, + 0.0, + 0.2, + -0.1, + 0.0 + ], + "3040000": [ + -0.2, + 0.1, + 0.2, + -0.1, + 0.0, + 0.0, + 0.0, + 0.3, + 0.0, + 0.0, + 0.1, + 0.0 + ], + "3080000": [ + 0.0, + 0.1, + 0.0, + 0.4, + 0.0, + 0.0, + 0.0, + -0.3, + 0.0, + 0.1, + 0.1, + 0.0 + ], + "3120000": [ + 0.1, + 0.2, + 0.0, + 0.2, + 0.0, + 0.0, + -0.1, + 0.0, + 0.0, + 0.0, + -0.1, + 0.0 + ], + "3160000": [ + -0.1, + -0.3, + 0.0, + -0.1, + 0.0, + 0.0, + 0.0, + 0.1, + 0.0, + 0.0, + -0.1, + 0.0 + ], + "3200000": [ + 0.1, + -0.2, + 0.0, + -0.3, + 0.0, + 0.0, + 0.2, + -0.1, + 0.0, + -0.1, + 0.1, + -0.1 + ], + "320000": [ + 0.2, + -0.4, + 0.0, + 0.3, + 0.0, + 0.0, + -0.1, + 0.1, + 0.0, + -0.1, + 0.0, + 0.0 + ], + "3240000": [ + 0.1, + 0.0, + -0.2, + 0.1, + 0.0, + 0.0, + -0.1, + -0.1, + 0.0, + 0.1, + 0.1, + 0.0 + ], + "3280000": [ + 0.0, + 0.0, + 0.3, + -0.1, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.3, + 0.0, + 0.0 + ], + "3320000": [ + 0.1, + 0.0, + -0.2, + -0.3, + 0.0, + 0.0, + -0.1, + -0.1, + 0.0, + -0.1, + 0.0, + 0.0 + ], + "3360000": [ + 0.0, + 0.4, + 0.3, + -0.1, + 0.0, + 0.0, + -0.3, + -0.2, + 0.0, + 0.1, + 0.3, + 0.0 + ], + "3400000": [ + 0.2, + 0.3, + 0.1, + 0.1, + 0.0, + 0.0, + -0.4, + 0.1, + 0.0, + 0.4, + 0.0, + 0.1 + ], + "3440000": [ + 0.0, + -0.2, + 0.0, + 0.1, + 0.0, + 0.0, + -0.3, + -0.1, + 0.0, + 0.0, + 0.2, + 0.0 + ], + "3480000": [ + 0.1, + 0.0, + 0.0, + 0.3, + 0.0, + 0.0, + -0.3, + -0.3, + 0.0, + 0.0, + -0.1, + 0.0 + ], + "3520000": [ + -0.2, + -0.2, + 0.3, + -0.4, + 0.0, + 0.0, + 0.0, + 0.1, + 0.0, + -0.1, + 0.0, + 0.0 + ], + "3560000": [ + 0.0, + 0.0, + -0.2, + 0.2, + 0.0, + 0.0, + 0.3, + 0.0, + 0.0, + 0.1, + 0.1, + 0.0 + ], + "3600000": [ + -0.3, + 0.0, + 0.1, + 0.1, + 0.0, + 0.0, + -0.2, + 0.3, + -0.1, + 0.0, + 0.0, + 0.0 + ], + "360000": [ + 0.0, + -0.1, + 0.0, + -0.1, + 0.0, + 0.0, + 0.1, + -0.1, + 0.0, + -0.1, + 0.1, + 0.0 + ], + "3640000": [ + 0.2, + -0.1, + 0.2, + -0.1, + 0.0, + 0.0, + -0.2, + 0.2, + 0.0, + -0.2, + 0.1, + 0.0 + ], + "3680000": [ + 0.1, + 0.2, + 0.5, + -0.1, + 0.0, + 0.0, + 0.1, + 0.2, + 0.0, + 0.2, + 0.0, + 0.0 + ], + "3720000": [ + -0.1, + 0.1, + 0.4, + 0.0, + 0.0, + 0.0, + 0.3, + -0.1, + 0.0, + 0.2, + 0.1, + 0.0 + ], + "3760000": [ + 0.0, + 0.0, + -0.1, + 0.0, + 0.0, + 0.0, + -0.1, + 0.3, + 0.0, + 0.2, + -0.1, + 0.0 + ], + "3800000": [ + 0.1, + -0.1, + 0.1, + -0.2, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.1, + 0.2, + 0.0 + ], + "3840000": [ + 0.0, + -0.1, + -0.2, + -0.1, + 0.0, + 0.0, + 0.0, + 0.2, + 0.0, + -0.2, + 0.0, + 0.0 + ], + "3880000": [ + 0.0, + 0.2, + -0.3, + -0.4, + 0.0, + 0.0, + -0.1, + -0.4, + 0.0, + 0.1, + -0.1, + 0.0 + ], + "3920000": [ + 0.0, + 0.0, + -0.2, + -0.2, + 0.0, + 0.0, + 0.1, + 0.2, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "3960000": [ + 0.0, + -0.1, + 0.1, + 0.2, + 0.0, + 0.0, + 0.4, + -0.1, + 0.0, + 0.1, + 0.0, + -0.1 + ], + "4000000": [ + 0.0, + 0.2, + 0.1, + 0.1, + 0.0, + 0.0, + -0.1, + 0.4, + 0.0, + 0.3, + 0.2, + 0.0 + ], + "400000": [ + 0.0, + -0.5, + 0.3, + -0.4, + 0.0, + 0.0, + 0.2, + -0.2, + 0.0, + -0.1, + 0.1, + 0.0 + ], + "40000": [ + -0.2, + -0.2, + 0.0, + 0.4, + 0.0, + 0.0, + -0.1, + 0.0, + 0.0, + 0.2, + 0.1, + 0.0 + ], + "4040000": [ + -0.1, + -0.2, + -0.1, + -0.2, + 0.0, + 0.0, + -0.3, + -0.3, + 0.0, + -0.1, + -0.1, + 0.0 + ], + "4080000": [ + 0.2, + 0.0, + 0.3, + 0.2, + 0.0, + 0.0, + -0.1, + -0.1, + 0.0, + 0.2, + 0.0, + 0.0 + ], + "4120000": [ + -0.1, + 0.1, + 0.1, + 0.3, + 0.0, + 0.0, + 0.1, + -0.2, + 0.0, + 0.0, + 0.1, + 0.0 + ], + "4160000": [ + 0.0, + 0.0, + -0.1, + 0.0, + 0.0, + 0.0, + 0.1, + -0.1, + 0.0, + 0.0, + -0.2, + 0.0 + ], + "4200000": [ + -0.1, + 0.0, + 0.3, + -0.1, + 0.0, + 0.0, + 0.1, + 0.1, + 0.0, + 0.3, + -0.1, + 0.0 + ], + "4280000": [ + 0.0, + 0.0, + 0.0, + 0.1, + 0.0, + 0.0, + -0.1, + -0.1, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "4320000": [ + 0.1, + -0.2, + -0.3, + -0.5, + 0.0, + 0.0, + 0.0, + 0.3, + 0.0, + 0.2, + 0.0, + 0.0 + ], + "4360000": [ + -0.1, + -0.1, + -0.3, + -0.1, + 0.0, + 0.0, + -0.2, + -0.1, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "440000": [ + 0.0, + 0.0, + 0.0, + -0.1, + 0.0, + 0.0, + 0.1, + 0.1, + 0.0, + 0.1, + 0.1, + -0.1 + ], + "480000": [ + 0.2, + -0.2, + 0.1, + -0.2, + 0.0, + 0.0, + -0.2, + 0.0, + 0.0, + 0.1, + 0.0, + 0.0 + ], + "520000": [ + 0.0, + 0.0, + 0.3, + 0.3, + 0.0, + 0.0, + 0.1, + 0.0, + 0.0, + -0.1, + -0.1, + 0.0 + ], + "560000": [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + -0.3, + 0.0, + 0.3, + 0.0, + 0.0 + ], + "600000": [ + 0.0, + 0.2, + -0.1, + -0.2, + 0.0, + 0.0, + 0.3, + 0.1, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "640000": [ + 0.0, + 0.4, + -0.3, + -0.1, + 0.0, + 0.0, + -0.1, + -0.1, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "680000": [ + 0.2, + -0.1, + 0.0, + -0.1, + 0.0, + 0.0, + 0.1, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "720000": [ + -0.1, + 0.0, + 0.0, + 0.3, + 0.0, + 0.0, + -0.1, + 0.1, + 0.0, + 0.2, + 0.0, + 0.0 + ], + "760000": [ + 0.1, + 0.0, + 0.0, + 0.1, + 0.0, + 0.0, + 0.0, + -0.2, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "800000": [ + -0.1, + 0.0, + -0.2, + 0.2, + 0.0, + 0.0, + -0.2, + 0.1, + 0.0, + -0.1, + 0.0, + 0.0 + ], + "80000": [ + -0.1, + 0.0, + 0.0, + -0.2, + 0.0, + 0.0, + 0.1, + 0.1, + 0.0, + -0.1, + 0.1, + 0.0 + ], + "840000": [ + 0.0, + 0.1, + -0.2, + -0.1, + 0.0, + 0.0, + 0.2, + 0.1, + 0.0, + -0.1, + 0.0, + 0.0 + ], + "880000": [ + 0.1, + -0.1, + 0.0, + 0.1, + 0.0, + 0.0, + -0.1, + 0.1, + 0.0, + 0.2, + 0.0, + 0.0 + ], + "920000": [ + -0.2, + -0.1, + -0.1, + -0.5, + 0.0, + 0.0, + -0.1, + -0.1, + 0.0, + 0.2, + 0.1, + 0.1 + ], + "960000": [ + -0.1, + -0.1, + -0.1, + -0.2, + 0.0, + 0.0, + 0.0, + 0.1, + 0.0, + 0.0, + 0.0, + 0.0 + ] + }, + "DR_PFSP": { + "0": [ + 0.1, + -0.1, + 0.0, + -0.2, + 0.0, + 0.0, + 0.1, + 0.1, + 0.0, + -0.1, + 0.0, + 0.0 + ], + "1000000": [ + 0.2, + -0.1, + 0.3, + 0.1, + 0.0, + 0.0, + -0.1, + -0.1, + 0.0, + 0.0, + -0.1, + 0.0 + ], + "1040000": [ + 0.0, + 0.2, + -0.1, + 0.7, + 0.0, + 0.0, + 0.1, + 0.2, + 0.0, + 0.1, + 0.1, + 0.0 + ], + "1080000": [ + -0.1, + 0.2, + 0.0, + -0.3, + 0.0, + 0.0, + 0.2, + 0.1, + 0.0, + 0.1, + -0.1, + 0.0 + ], + "1120000": [ + 0.1, + -0.3, + -0.3, + -0.1, + 0.0, + 0.0, + 0.1, + 0.0, + 0.0, + -0.2, + 0.0, + 0.0 + ], + "1160000": [ + 0.4, + 0.2, + 0.2, + 0.1, + 0.0, + 0.0, + -0.1, + -0.1, + 0.0, + 0.1, + -0.1, + 0.0 + ], + "1200000": [ + -0.3, + 0.3, + 0.3, + 0.3, + 0.0, + 0.0, + -0.2, + 0.0, + 0.0, + 0.1, + 0.1, + 0.0 + ], + "120000": [ + 0.1, + 0.2, + 0.1, + 0.1, + 0.0, + 0.0, + -0.1, + 0.0, + 0.0, + 0.1, + 0.0, + 0.0 + ], + "1240000": [ + -0.1, + -0.1, + -0.1, + -0.6, + 0.0, + 0.0, + 0.0, + -0.1, + 0.0, + 0.0, + -0.1, + 0.0 + ], + "1280000": [ + 0.0, + 0.0, + -0.1, + -0.1, + 0.0, + 0.0, + 0.3, + 0.0, + 0.0, + 0.2, + 0.0, + 0.0 + ], + "1320000": [ + 0.2, + 0.1, + 0.0, + 0.2, + 0.0, + 0.0, + 0.2, + 0.1, + 0.0, + 0.0, + 0.0, + -0.1 + ], + "1360000": [ + 0.1, + 0.0, + 0.1, + -0.3, + 0.0, + 0.0, + 0.1, + 0.0, + 0.0, + 0.2, + 0.0, + 0.0 + ], + "1400000": [ + -0.1, + 0.0, + 0.2, + 0.2, + 0.0, + 0.0, + -0.1, + 0.1, + 0.0, + -0.1, + 0.0, + 0.0 + ], + "1440000": [ + 0.3, + -0.4, + 0.3, + 0.1, + 0.0, + 0.0, + 0.0, + 0.2, + 0.0, + 0.4, + 0.0, + 0.0 + ], + "1480000": [ + -0.1, + 0.1, + 0.1, + -0.5, + 0.0, + 0.0, + 0.0, + 0.1, + 0.0, + -0.1, + 0.0, + 0.0 + ], + "1520000": [ + 0.2, + -0.1, + 0.4, + -0.5, + 0.0, + 0.0, + 0.0, + 0.1, + 0.0, + -0.2, + 0.0, + 0.0 + ], + "1560000": [ + 0.1, + -0.1, + 0.3, + 0.4, + 0.0, + 0.0, + 0.1, + -0.2, + 0.0, + 0.1, + 0.0, + -0.1 + ], + "1600000": [ + 0.2, + -0.1, + -0.1, + 0.1, + 0.0, + 0.0, + -0.1, + 0.0, + 0.0, + 0.0, + -0.1, + 0.0 + ], + "160000": [ + -0.1, + -0.1, + -0.2, + 0.0, + 0.0, + 0.0, + 0.0, + 0.1, + 0.0, + -0.3, + 0.0, + 0.0 + ], + "1640000": [ + 0.0, + 0.1, + -0.1, + -0.1, + 0.0, + 0.0, + 0.1, + 0.2, + 0.0, + 0.2, + -0.1, + 0.0 + ], + "1680000": [ + 0.2, + 0.0, + -0.2, + 0.2, + 0.0, + 0.0, + -0.3, + 0.0, + 0.0, + 0.1, + 0.0, + 0.0 + ], + "1720000": [ + 0.0, + 0.0, + -0.1, + -0.1, + 0.0, + 0.0, + 0.1, + -0.1, + 0.0, + 0.2, + 0.0, + 0.1 + ], + "1760000": [ + 0.1, + -0.2, + -0.4, + -0.4, + 0.0, + 0.0, + 0.1, + 0.1, + 0.0, + -0.1, + 0.0, + -0.1 + ], + "1800000": [ + 0.2, + 0.0, + 0.1, + -0.3, + 0.0, + 0.0, + -0.4, + 0.2, + 0.0, + -0.1, + -0.1, + 0.0 + ], + "1840000": [ + 0.0, + -0.3, + 0.1, + 0.1, + 0.0, + 0.0, + 0.1, + -0.1, + 0.0, + -0.2, + -0.1, + 0.0 + ], + "1880000": [ + 0.1, + -0.2, + -0.2, + -0.1, + 0.0, + 0.0, + 0.2, + 0.1, + 0.0, + -0.1, + 0.0, + 0.0 + ], + "1920000": [ + 0.3, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + -0.2, + 0.2, + 0.0, + 0.0, + -0.1, + 0.0 + ], + "1960000": [ + 0.1, + -0.4, + -0.1, + -0.2, + 0.0, + 0.0, + 0.1, + 0.1, + 0.0, + -0.2, + 0.3, + 0.0 + ], + "2000000": [ + 0.1, + -0.1, + -0.1, + -0.1, + 0.0, + 0.0, + 0.2, + 0.0, + 0.0, + 0.1, + 0.0, + 0.0 + ], + "200000": [ + 0.0, + -0.2, + 0.0, + -0.1, + 0.0, + 0.0, + 0.1, + 0.1, + 0.0, + -0.2, + 0.0, + 0.0 + ], + "2040000": [ + 0.1, + 0.0, + -0.4, + -0.4, + 0.0, + 0.0, + -0.2, + -0.2, + 0.0, + -0.1, + 0.1, + 0.0 + ], + "2080000": [ + -0.1, + 0.0, + -0.3, + -0.2, + 0.0, + 0.0, + 0.1, + -0.1, + 0.0, + -0.2, + 0.1, + 0.0 + ], + "2120000": [ + -0.1, + -0.1, + -0.1, + -0.4, + 0.0, + 0.0, + 0.0, + -0.1, + 0.0, + -0.4, + -0.2, + 0.0 + ], + "2160000": [ + 0.1, + 0.3, + 0.3, + -0.2, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.1, + 0.0, + 0.0 + ], + "2200000": [ + 0.1, + -0.1, + -0.3, + -0.1, + 0.0, + 0.0, + 0.0, + -0.2, + 0.0, + 0.2, + 0.0, + 0.0 + ], + "2240000": [ + -0.3, + 0.3, + -0.1, + 0.2, + 0.0, + 0.0, + 0.1, + -0.1, + 0.0, + -0.2, + 0.0, + 0.0 + ], + "2280000": [ + 0.0, + 0.0, + 0.2, + 0.0, + 0.0, + 0.0, + -0.3, + -0.2, + 0.0, + 0.0, + 0.1, + 0.0 + ], + "2320000": [ + 0.0, + -0.3, + 0.1, + -0.5, + 0.0, + 0.0, + 0.1, + 0.2, + 0.0, + 0.2, + -0.1, + 0.1 + ], + "2360000": [ + -0.1, + 0.1, + 0.1, + 0.1, + 0.0, + 0.0, + 0.3, + 0.0, + 0.0, + 0.1, + 0.0, + 0.0 + ], + "2400000": [ + 0.3, + 0.0, + -0.1, + -0.1, + 0.0, + 0.0, + -0.1, + -0.1, + 0.0, + -0.1, + -0.1, + -0.1 + ], + "240000": [ + 0.0, + -0.2, + -0.1, + -0.4, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "2440000": [ + 0.2, + 0.0, + -0.1, + -0.1, + 0.0, + 0.0, + 0.1, + -0.3, + 0.0, + 0.0, + 0.1, + 0.0 + ], + "2480000": [ + 0.1, + 0.2, + 0.1, + -0.4, + 0.0, + 0.0, + 0.0, + -0.2, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "2520000": [ + -0.1, + 0.2, + 0.2, + -0.1, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + -0.1, + 0.0, + 0.0 + ], + "2560000": [ + 0.0, + -0.2, + -0.2, + 0.2, + 0.0, + 0.0, + -0.2, + 0.0, + 0.1, + 0.2, + 0.0, + 0.0 + ], + "2600000": [ + 0.0, + 0.2, + 0.0, + 0.0, + 0.0, + 0.0, + 0.2, + -0.3, + 0.0, + -0.4, + 0.2, + 0.1 + ], + "2640000": [ + 0.0, + -0.2, + 0.1, + 0.0, + 0.0, + 0.0, + -0.1, + 0.2, + 0.0, + -0.1, + -0.1, + 0.0 + ], + "2680000": [ + 0.2, + 0.1, + -0.2, + 0.5, + 0.0, + 0.0, + 0.2, + 0.0, + 0.0, + 0.2, + -0.1, + 0.0 + ], + "2720000": [ + -0.1, + 0.0, + -0.2, + -0.2, + 0.0, + 0.0, + -0.1, + -0.1, + 0.0, + -0.2, + 0.1, + 0.0 + ], + "2760000": [ + 0.0, + 0.1, + 0.2, + 0.2, + 0.0, + 0.0, + 0.0, + -0.1, + 0.0, + -0.1, + -0.1, + 0.1 + ], + "2800000": [ + -0.1, + 0.1, + 0.1, + -0.4, + 0.0, + 0.0, + 0.2, + 0.1, + 0.0, + 0.2, + 0.0, + 0.0 + ], + "280000": [ + -0.2, + 0.3, + 0.0, + -0.1, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.1, + 0.0 + ], + "2840000": [ + 0.3, + 0.1, + 0.3, + -0.3, + 0.0, + 0.0, + 0.0, + -0.4, + 0.0, + 0.2, + 0.0, + 0.0 + ], + "2880000": [ + -0.1, + 0.1, + 0.0, + -0.2, + 0.0, + 0.0, + 0.1, + 0.0, + 0.0, + -0.1, + 0.0, + 0.1 + ], + "2920000": [ + 0.0, + 0.1, + 0.0, + 0.1, + 0.0, + 0.0, + 0.1, + 0.0, + 0.0, + 0.2, + 0.3, + 0.0 + ], + "2960000": [ + 0.4, + 0.0, + -0.1, + 0.6, + 0.0, + 0.0, + -0.1, + -0.3, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "3000000": [ + 0.2, + -0.1, + 0.3, + -0.1, + 0.0, + 0.0, + -0.4, + 0.2, + 0.0, + -0.2, + 0.1, + 0.0 + ], + "3040000": [ + 0.2, + -0.1, + -0.2, + 0.1, + 0.0, + 0.0, + 0.0, + -0.3, + 0.0, + 0.0, + -0.1, + 0.0 + ], + "3080000": [ + 0.0, + -0.1, + 0.0, + -0.4, + 0.0, + 0.0, + 0.0, + 0.3, + 0.0, + -0.1, + -0.1, + 0.0 + ], + "3120000": [ + -0.1, + -0.2, + 0.0, + -0.2, + 0.0, + 0.0, + 0.1, + 0.0, + 0.0, + 0.0, + 0.1, + 0.0 + ], + "3160000": [ + 0.1, + 0.3, + 0.0, + 0.1, + 0.0, + 0.0, + 0.0, + -0.1, + 0.0, + 0.0, + 0.1, + 0.0 + ], + "3200000": [ + -0.1, + 0.2, + 0.0, + 0.3, + 0.0, + 0.0, + -0.2, + 0.1, + 0.0, + 0.1, + -0.1, + 0.1 + ], + "320000": [ + -0.2, + 0.4, + 0.0, + -0.3, + 0.0, + 0.0, + 0.1, + -0.1, + 0.0, + 0.1, + 0.0, + 0.0 + ], + "3240000": [ + -0.1, + 0.0, + 0.2, + -0.1, + 0.0, + 0.0, + 0.1, + 0.1, + 0.0, + -0.1, + -0.1, + 0.0 + ], + "3280000": [ + 0.0, + 0.0, + -0.3, + 0.1, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + -0.3, + 0.0, + 0.0 + ], + "3320000": [ + -0.1, + 0.0, + 0.2, + 0.3, + 0.0, + 0.0, + 0.1, + 0.1, + 0.0, + 0.1, + 0.0, + 0.0 + ], + "3360000": [ + 0.0, + -0.4, + -0.3, + 0.1, + 0.0, + 0.0, + 0.3, + 0.2, + 0.0, + -0.1, + -0.3, + 0.0 + ], + "3400000": [ + -0.2, + -0.3, + -0.1, + -0.1, + 0.0, + 0.0, + 0.4, + -0.1, + 0.0, + -0.4, + 0.0, + -0.1 + ], + "3440000": [ + 0.0, + 0.2, + 0.0, + -0.1, + 0.0, + 0.0, + 0.3, + 0.1, + 0.0, + 0.0, + -0.2, + 0.0 + ], + "3480000": [ + -0.1, + 0.0, + 0.0, + -0.3, + 0.0, + 0.0, + 0.3, + 0.3, + 0.0, + 0.0, + 0.1, + 0.0 + ], + "3520000": [ + 0.2, + 0.2, + -0.3, + 0.4, + 0.0, + 0.0, + 0.0, + -0.1, + 0.0, + 0.1, + 0.0, + 0.0 + ], + "3560000": [ + 0.0, + 0.0, + 0.2, + -0.2, + 0.0, + 0.0, + -0.3, + 0.0, + 0.0, + -0.1, + -0.1, + 0.0 + ], + "3600000": [ + 0.3, + 0.0, + -0.1, + -0.1, + 0.0, + 0.0, + 0.2, + -0.3, + 0.1, + 0.0, + 0.0, + 0.0 + ], + "360000": [ + 0.0, + 0.1, + 0.0, + 0.1, + 0.0, + 0.0, + -0.1, + 0.1, + 0.0, + 0.1, + -0.1, + 0.0 + ], + "3640000": [ + -0.2, + 0.1, + -0.2, + 0.1, + 0.0, + 0.0, + 0.2, + -0.2, + 0.0, + 0.2, + -0.1, + 0.0 + ], + "3680000": [ + -0.1, + -0.2, + -0.5, + 0.1, + 0.0, + 0.0, + -0.1, + -0.2, + 0.0, + -0.2, + 0.0, + 0.0 + ], + "3720000": [ + 0.1, + -0.1, + -0.4, + 0.0, + 0.0, + 0.0, + -0.3, + 0.1, + 0.0, + -0.2, + -0.1, + 0.0 + ], + "3760000": [ + 0.0, + 0.0, + 0.1, + 0.0, + 0.0, + 0.0, + 0.1, + -0.3, + 0.0, + -0.2, + 0.1, + 0.0 + ], + "3800000": [ + -0.1, + 0.1, + -0.1, + 0.2, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + -0.1, + -0.2, + 0.0 + ], + "3840000": [ + 0.0, + 0.1, + 0.2, + 0.1, + 0.0, + 0.0, + 0.0, + -0.2, + 0.0, + 0.2, + 0.0, + 0.0 + ], + "3880000": [ + 0.0, + -0.2, + 0.3, + 0.4, + 0.0, + 0.0, + 0.1, + 0.4, + 0.0, + -0.1, + 0.1, + 0.0 + ], + "3920000": [ + 0.0, + 0.0, + 0.2, + 0.2, + 0.0, + 0.0, + -0.1, + -0.2, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "3960000": [ + 0.0, + 0.1, + -0.1, + -0.2, + 0.0, + 0.0, + -0.4, + 0.1, + 0.0, + -0.1, + 0.0, + 0.1 + ], + "4000000": [ + 0.0, + -0.2, + -0.1, + -0.1, + 0.0, + 0.0, + 0.1, + -0.4, + 0.0, + -0.3, + -0.2, + 0.0 + ], + "400000": [ + 0.0, + 0.5, + -0.3, + 0.4, + 0.0, + 0.0, + -0.2, + 0.2, + 0.0, + 0.1, + -0.1, + 0.0 + ], + "40000": [ + 0.2, + 0.2, + 0.0, + -0.4, + 0.0, + 0.0, + 0.1, + 0.0, + 0.0, + -0.2, + -0.1, + 0.0 + ], + "4040000": [ + 0.1, + 0.2, + 0.1, + 0.2, + 0.0, + 0.0, + 0.3, + 0.3, + 0.0, + 0.1, + 0.1, + 0.0 + ], + "4080000": [ + -0.2, + 0.0, + -0.3, + -0.2, + 0.0, + 0.0, + 0.1, + 0.1, + 0.0, + -0.2, + 0.0, + 0.0 + ], + "4120000": [ + 0.1, + -0.1, + -0.1, + -0.3, + 0.0, + 0.0, + -0.1, + 0.2, + 0.0, + 0.0, + -0.1, + 0.0 + ], + "4160000": [ + 0.0, + 0.0, + 0.1, + 0.0, + 0.0, + 0.0, + -0.1, + 0.1, + 0.0, + 0.0, + 0.2, + 0.0 + ], + "4200000": [ + 0.1, + 0.0, + -0.3, + 0.1, + 0.0, + 0.0, + -0.1, + -0.1, + 0.0, + -0.3, + 0.1, + 0.0 + ], + "4280000": [ + 0.0, + 0.0, + 0.0, + -0.1, + 0.0, + 0.0, + 0.1, + 0.1, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "4320000": [ + -0.1, + 0.2, + 0.3, + 0.5, + 0.0, + 0.0, + 0.0, + -0.3, + 0.0, + -0.2, + 0.0, + 0.0 + ], + "4360000": [ + 0.1, + 0.1, + 0.3, + 0.1, + 0.0, + 0.0, + 0.2, + 0.1, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "440000": [ + 0.0, + 0.0, + 0.0, + 0.1, + 0.0, + 0.0, + -0.1, + -0.1, + 0.0, + -0.1, + -0.1, + 0.1 + ], + "480000": [ + -0.2, + 0.2, + -0.1, + 0.2, + 0.0, + 0.0, + 0.2, + 0.0, + 0.0, + -0.1, + 0.0, + 0.0 + ], + "520000": [ + 0.0, + 0.0, + -0.3, + -0.3, + 0.0, + 0.0, + -0.1, + 0.0, + 0.0, + 0.1, + 0.1, + 0.0 + ], + "560000": [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.3, + 0.0, + -0.3, + 0.0, + 0.0 + ], + "600000": [ + 0.0, + -0.2, + 0.1, + 0.2, + 0.0, + 0.0, + -0.3, + -0.1, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "640000": [ + 0.0, + -0.4, + 0.3, + 0.1, + 0.0, + 0.0, + 0.1, + 0.1, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "680000": [ + -0.2, + 0.1, + 0.0, + 0.1, + 0.0, + 0.0, + -0.1, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "720000": [ + 0.1, + 0.0, + 0.0, + -0.3, + 0.0, + 0.0, + 0.1, + -0.1, + 0.0, + -0.2, + 0.0, + 0.0 + ], + "760000": [ + -0.1, + 0.0, + 0.0, + -0.1, + 0.0, + 0.0, + 0.0, + 0.2, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "800000": [ + 0.1, + 0.0, + 0.2, + -0.2, + 0.0, + 0.0, + 0.2, + -0.1, + 0.0, + 0.1, + 0.0, + 0.0 + ], + "80000": [ + 0.1, + 0.0, + 0.0, + 0.2, + 0.0, + 0.0, + -0.1, + -0.1, + 0.0, + 0.1, + -0.1, + 0.0 + ], + "840000": [ + 0.0, + -0.1, + 0.2, + 0.1, + 0.0, + 0.0, + -0.2, + -0.1, + 0.0, + 0.1, + 0.0, + 0.0 + ], + "880000": [ + -0.1, + 0.1, + 0.0, + -0.1, + 0.0, + 0.0, + 0.1, + -0.1, + 0.0, + -0.2, + 0.0, + 0.0 + ], + "920000": [ + 0.2, + 0.1, + 0.1, + 0.5, + 0.0, + 0.0, + 0.1, + 0.1, + 0.0, + -0.2, + -0.1, + -0.1 + ], + "960000": [ + 0.1, + 0.1, + 0.1, + 0.2, + 0.0, + 0.0, + 0.0, + -0.1, + 0.0, + 0.0, + 0.0, + 0.0 + ] + } +} \ No newline at end of file diff --git a/syllabus/examples/experimental/round_robin/parse_results/DR_SP_DR_FSP_returns.json b/syllabus/examples/experimental/round_robin/parse_results/DR_SP_DR_FSP_returns.json new file mode 100644 index 00000000..f04f00ee --- /dev/null +++ b/syllabus/examples/experimental/round_robin/parse_results/DR_SP_DR_FSP_returns.json @@ -0,0 +1,98 @@ +{ + "DR_SP": { + "0": [0.3, 0.0, -0.1, 0.1, 0.0, 0.0, 0.2, -0.1, 0.0, 0.1, 0.0, 0.0], + "1000000": [0.0, -0.1, 0.0, -0.1, 0.0, 0.0, -0.2, -0.3, 0.0, 0.0, 0.0, 0.0], + "1040000": [ + -0.1, -0.2, -0.2, -0.2, 0.0, 0.0, 0.0, 0.0, 0.0, 0.1, -0.1, 0.0 + ], + "1080000": [-0.1, -0.3, 0.0, -0.3, 0.0, 0.0, 0.0, 0.2, 0.0, 0.0, 0.0, 0.0], + "1120000": [0.0, -0.2, -0.4, 0.3, 0.0, 0.0, 0.0, 0.0, 0.0, -0.1, 0.0, 0.0], + "1160000": [-0.2, 0.1, -0.1, 0.1, 0.0, 0.0, 0.1, -0.1, 0.0, 0.0, 0.0, 0.0], + "1200000": [0.1, 0.1, 0.1, -0.3, 0.0, 0.0, -0.1, -0.4, 0.0, 0.1, 0.1, 0.0], + "120000": [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.1, -0.1, 0.0, 0.0, 0.0, -0.1], + "1240000": [0.2, 0.1, 0.0, -0.3, 0.0, 0.0, 0.1, -0.1, 0.0, -0.1, 0.0, 0.0], + "1280000": [0.0, -0.1, 0.3, -0.1, 0.0, 0.0, 0.0, -0.1, 0.0, 0.0, -0.1, 0.0], + "1320000": [ + 0.3, -0.2, 0.2, -0.6, 0.0, 0.0, -0.3, 0.2, 0.0, -0.1, -0.1, 0.0 + ], + "1360000": [-0.1, -0.5, 0.1, -0.2, 0.0, 0.0, -0.1, 0.2, 0.0, 0.3, 0.0, 0.0], + "1400000": [0.2, 0.2, 0.1, 0.0, 0.0, 0.0, -0.3, -0.1, 0.0, 0.1, 0.1, 0.0], + "1440000": [-0.1, -0.1, -0.1, -0.3, 0.0, 0.0, 0.0, 0.1, 0.0, 0.3, 0.0, 0.0], + "1480000": [-0.1, -0.2, 0.0, 0.2, 0.0, 0.0, 0.1, 0.1, 0.0, 0.1, 0.0, 0.0], + "1520000": [0.2, 0.1, -0.1, 0.1, 0.0, 0.0, 0.4, 0.2, 0.0, -0.1, 0.0, 0.0], + "1560000": [ + -0.1, 0.1, -0.1, -0.1, 0.0, 0.0, 0.1, -0.1, 0.0, -0.1, 0.1, 0.0 + ], + "160000": [0.0, 0.1, 0.2, -0.2, 0.0, 0.0, -0.1, 0.1, 0.0, -0.1, 0.0, 0.0], + "200000": [0.1, 0.2, 0.0, -0.2, 0.0, 0.0, 0.0, 0.2, 0.0, 0.0, 0.0, 0.0], + "240000": [-0.1, 0.0, 0.2, -0.3, 0.0, 0.0, 0.0, 0.1, 0.0, 0.0, -0.1, 0.0], + "280000": [0.1, -0.2, 0.1, 0.1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.1, 0.0, 0.0], + "320000": [-0.1, -0.1, 0.1, -0.1, 0.0, 0.0, 0.2, 0.0, 0.0, 0.2, 0.0, 0.0], + "360000": [0.3, -0.3, 0.0, -0.1, 0.0, 0.0, 0.1, -0.2, 0.0, 0.0, 0.0, 0.0], + "400000": [-0.2, -0.1, -0.3, -0.1, 0.0, 0.0, -0.1, 0.2, 0.0, 0.0, 0.0, 0.0], + "40000": [-0.3, 0.0, -0.2, 0.4, 0.0, 0.0, -0.1, -0.1, 0.0, 0.3, 0.0, 0.0], + "440000": [0.1, -0.1, -0.2, 0.1, 0.0, 0.0, -0.1, -0.1, 0.0, 0.0, 0.0, -0.1], + "480000": [-0.1, 0.1, -0.2, -0.3, 0.0, 0.0, -0.1, 0.1, 0.0, 0.0, 0.0, 0.0], + "520000": [0.0, -0.1, 0.1, -0.2, 0.0, 0.0, 0.0, 0.2, 0.0, -0.1, 0.2, 0.0], + "560000": [-0.1, 0.0, -0.1, 0.2, 0.0, 0.0, -0.4, 0.1, 0.0, 0.0, 0.1, 0.0], + "600000": [0.1, -0.2, -0.1, 0.0, 0.0, 0.0, 0.1, -0.3, 0.0, 0.0, 0.1, 0.0], + "640000": [-0.1, 0.0, 0.0, -0.2, 0.0, 0.0, 0.0, 0.1, 0.0, -0.1, 0.0, 0.0], + "680000": [0.0, 0.2, 0.1, -0.4, 0.0, 0.0, -0.2, -0.1, 0.0, -0.1, 0.0, 0.0], + "720000": [-0.1, 0.0, -0.2, 0.0, 0.0, 0.0, 0.0, 0.2, 0.0, 0.0, 0.1, 0.0], + "760000": [0.0, -0.1, 0.2, 0.0, 0.0, 0.0, 0.2, -0.1, 0.0, -0.1, 0.0, 0.0], + "800000": [-0.2, 0.0, 0.3, -0.1, 0.0, 0.0, -0.2, -0.2, 0.0, 0.0, 0.0, 0.0], + "80000": [0.3, 0.2, 0.1, -0.2, 0.0, 0.0, 0.0, 0.0, 0.0, -0.2, 0.1, 0.0], + "840000": [0.1, -0.1, -0.2, 0.1, 0.0, 0.0, 0.1, -0.1, 0.0, 0.1, -0.1, 0.0], + "880000": [0.1, -0.1, 0.0, 0.3, 0.0, 0.0, 0.0, 0.0, 0.0, 0.1, -0.1, 0.0], + "920000": [-0.2, 0.1, 0.0, -0.5, 0.0, 0.0, -0.2, 0.0, 0.0, 0.0, 0.1, 0.1], + "960000": [0.0, 0.0, -0.1, -0.1, 0.0, 0.0, 0.1, -0.1, 0.0, 0.2, 0.0, 0.0] + }, + "DR_FSP": { + "0": [-0.3, 0.0, 0.1, -0.1, 0.0, 0.0, -0.2, 0.1, 0.0, -0.1, 0.0, 0.0], + "1000000": [0.0, 0.1, 0.0, 0.1, 0.0, 0.0, 0.2, 0.3, 0.0, 0.0, 0.0, 0.0], + "1040000": [0.1, 0.2, 0.2, 0.2, 0.0, 0.0, 0.0, 0.0, 0.0, -0.1, 0.1, 0.0], + "1080000": [0.1, 0.3, 0.0, 0.3, 0.0, 0.0, 0.0, -0.2, 0.0, 0.0, 0.0, 0.0], + "1120000": [0.0, 0.2, 0.4, -0.3, 0.0, 0.0, 0.0, 0.0, 0.0, 0.1, 0.0, 0.0], + "1160000": [0.2, -0.1, 0.1, -0.1, 0.0, 0.0, -0.1, 0.1, 0.0, 0.0, 0.0, 0.0], + "1200000": [ + -0.1, -0.1, -0.1, 0.3, 0.0, 0.0, 0.1, 0.4, 0.0, -0.1, -0.1, 0.0 + ], + "120000": [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.1, 0.1, 0.0, 0.0, 0.0, 0.1], + "1240000": [-0.2, -0.1, 0.0, 0.3, 0.0, 0.0, -0.1, 0.1, 0.0, 0.1, 0.0, 0.0], + "1280000": [0.0, 0.1, -0.3, 0.1, 0.0, 0.0, 0.0, 0.1, 0.0, 0.0, 0.1, 0.0], + "1320000": [-0.3, 0.2, -0.2, 0.6, 0.0, 0.0, 0.3, -0.2, 0.0, 0.1, 0.1, 0.0], + "1360000": [0.1, 0.5, -0.1, 0.2, 0.0, 0.0, 0.1, -0.2, 0.0, -0.3, 0.0, 0.0], + "1400000": [ + -0.2, -0.2, -0.1, 0.0, 0.0, 0.0, 0.3, 0.1, 0.0, -0.1, -0.1, 0.0 + ], + "1440000": [0.1, 0.1, 0.1, 0.3, 0.0, 0.0, 0.0, -0.1, 0.0, -0.3, 0.0, 0.0], + "1480000": [0.1, 0.2, 0.0, -0.2, 0.0, 0.0, -0.1, -0.1, 0.0, -0.1, 0.0, 0.0], + "1520000": [ + -0.2, -0.1, 0.1, -0.1, 0.0, 0.0, -0.4, -0.2, 0.0, 0.1, 0.0, 0.0 + ], + "1560000": [0.1, -0.1, 0.1, 0.1, 0.0, 0.0, -0.1, 0.1, 0.0, 0.1, -0.1, 0.0], + "160000": [0.0, -0.1, -0.2, 0.2, 0.0, 0.0, 0.1, -0.1, 0.0, 0.1, 0.0, 0.0], + "200000": [-0.1, -0.2, 0.0, 0.2, 0.0, 0.0, 0.0, -0.2, 0.0, 0.0, 0.0, 0.0], + "240000": [0.1, 0.0, -0.2, 0.3, 0.0, 0.0, 0.0, -0.1, 0.0, 0.0, 0.1, 0.0], + "280000": [-0.1, 0.2, -0.1, -0.1, 0.0, 0.0, 0.0, 0.0, 0.0, -0.1, 0.0, 0.0], + "320000": [0.1, 0.1, -0.1, 0.1, 0.0, 0.0, -0.2, 0.0, 0.0, -0.2, 0.0, 0.0], + "360000": [-0.3, 0.3, 0.0, 0.1, 0.0, 0.0, -0.1, 0.2, 0.0, 0.0, 0.0, 0.0], + "400000": [0.2, 0.1, 0.3, 0.1, 0.0, 0.0, 0.1, -0.2, 0.0, 0.0, 0.0, 0.0], + "40000": [0.3, 0.0, 0.2, -0.4, 0.0, 0.0, 0.1, 0.1, 0.0, -0.3, 0.0, 0.0], + "440000": [-0.1, 0.1, 0.2, -0.1, 0.0, 0.0, 0.1, 0.1, 0.0, 0.0, 0.0, 0.1], + "480000": [0.1, -0.1, 0.2, 0.3, 0.0, 0.0, 0.1, -0.1, 0.0, 0.0, 0.0, 0.0], + "520000": [0.0, 0.1, -0.1, 0.2, 0.0, 0.0, 0.0, -0.2, 0.0, 0.1, -0.2, 0.0], + "560000": [0.1, 0.0, 0.1, -0.2, 0.0, 0.0, 0.4, -0.1, 0.0, 0.0, -0.1, 0.0], + "600000": [-0.1, 0.2, 0.1, 0.0, 0.0, 0.0, -0.1, 0.3, 0.0, 0.0, -0.1, 0.0], + "640000": [0.1, 0.0, 0.0, 0.2, 0.0, 0.0, 0.0, -0.1, 0.0, 0.1, 0.0, 0.0], + "680000": [0.0, -0.2, -0.1, 0.4, 0.0, 0.0, 0.2, 0.1, 0.0, 0.1, 0.0, 0.0], + "720000": [0.1, 0.0, 0.2, 0.0, 0.0, 0.0, 0.0, -0.2, 0.0, 0.0, -0.1, 0.0], + "760000": [0.0, 0.1, -0.2, 0.0, 0.0, 0.0, -0.2, 0.1, 0.0, 0.1, 0.0, 0.0], + "800000": [0.2, 0.0, -0.3, 0.1, 0.0, 0.0, 0.2, 0.2, 0.0, 0.0, 0.0, 0.0], + "80000": [-0.3, -0.2, -0.1, 0.2, 0.0, 0.0, 0.0, 0.0, 0.0, 0.2, -0.1, 0.0], + "840000": [-0.1, 0.1, 0.2, -0.1, 0.0, 0.0, -0.1, 0.1, 0.0, -0.1, 0.1, 0.0], + "880000": [-0.1, 0.1, 0.0, -0.3, 0.0, 0.0, 0.0, 0.0, 0.0, -0.1, 0.1, 0.0], + "920000": [0.2, -0.1, 0.0, 0.5, 0.0, 0.0, 0.2, 0.0, 0.0, 0.0, -0.1, -0.1], + "960000": [0.0, 0.0, 0.1, 0.1, 0.0, 0.0, -0.1, 0.1, 0.0, -0.2, 0.0, 0.0] + } +} diff --git a/syllabus/examples/experimental/round_robin/parse_results/DR_SP_DR_PFSP_returns.json b/syllabus/examples/experimental/round_robin/parse_results/DR_SP_DR_PFSP_returns.json new file mode 100644 index 00000000..6acad350 --- /dev/null +++ b/syllabus/examples/experimental/round_robin/parse_results/DR_SP_DR_PFSP_returns.json @@ -0,0 +1,1126 @@ +{ + "DR_SP": { + "0": [ + -0.2, + -0.2, + 0.1, + 0.4, + 0.0, + 0.0, + 0.2, + -0.1, + 0.0, + -0.2, + 0.0, + 0.0 + ], + "1000000": [ + 0.0, + 0.0, + -0.2, + 0.4, + 0.0, + 0.0, + 0.1, + -0.3, + 0.0, + 0.0, + 0.1, + -0.1 + ], + "1040000": [ + 0.0, + 0.1, + 0.0, + 0.3, + 0.0, + 0.0, + 0.1, + -0.2, + 0.0, + -0.1, + -0.1, + 0.0 + ], + "1080000": [ + -0.2, + -0.2, + 0.0, + -0.3, + 0.0, + 0.0, + 0.2, + 0.5, + 0.0, + 0.0, + -0.1, + 0.0 + ], + "1120000": [ + 0.1, + -0.4, + -0.2, + 0.4, + 0.0, + 0.0, + 0.3, + 0.0, + 0.0, + 0.3, + 0.0, + 0.0 + ], + "1160000": [ + 0.1, + -0.1, + 0.2, + 0.1, + 0.0, + 0.0, + 0.2, + 0.1, + 0.0, + 0.1, + 0.0, + 0.0 + ], + "1200000": [ + -0.1, + 0.2, + -0.3, + -0.2, + 0.0, + 0.0, + 0.1, + 0.0, + 0.0, + -0.2, + 0.0, + 0.0 + ], + "120000": [ + 0.0, + 0.1, + 0.0, + 0.0, + 0.0, + 0.0, + -0.2, + 0.1, + 0.0, + -0.1, + -0.1, + 0.0 + ], + "1240000": [ + 0.1, + -0.1, + 0.0, + 0.1, + 0.0, + 0.0, + 0.1, + 0.0, + 0.0, + 0.2, + -0.2, + 0.0 + ], + "1280000": [ + 0.0, + 0.2, + -0.1, + 0.0, + 0.0, + 0.0, + -0.1, + 0.0, + 0.0, + 0.2, + 0.0, + 0.0 + ], + "1320000": [ + 0.1, + 0.0, + 0.0, + 0.1, + 0.0, + 0.0, + 0.2, + -0.3, + 0.0, + 0.2, + -0.2, + 0.0 + ], + "1360000": [ + 0.0, + 0.0, + -0.1, + 0.1, + 0.0, + 0.0, + 0.0, + -0.3, + 0.0, + 0.1, + 0.0, + 0.0 + ], + "1400000": [ + -0.1, + -0.1, + 0.0, + -0.6, + 0.0, + 0.0, + 0.1, + 0.1, + 0.0, + -0.2, + 0.0, + 0.0 + ], + "1440000": [ + 0.1, + -0.1, + 0.2, + -0.1, + 0.0, + 0.0, + 0.0, + 0.1, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "1480000": [ + -0.1, + 0.1, + -0.3, + 0.3, + 0.0, + 0.0, + 0.1, + -0.1, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "1520000": [ + 0.0, + 0.0, + 0.1, + -0.6, + 0.0, + 0.0, + 0.3, + 0.0, + 0.0, + -0.3, + 0.1, + 0.0 + ], + "1560000": [ + 0.1, + -0.1, + 0.0, + 0.1, + 0.0, + 0.0, + -0.1, + 0.2, + 0.0, + 0.0, + -0.1, + 0.1 + ], + "160000": [ + 0.1, + 0.2, + 0.0, + -0.3, + 0.0, + 0.0, + -0.1, + 0.0, + 0.0, + -0.2, + -0.1, + 0.0 + ], + "200000": [ + -0.3, + -0.1, + -0.1, + -0.2, + 0.0, + 0.0, + -0.1, + 0.2, + 0.0, + -0.1, + -0.2, + 0.0 + ], + "240000": [ + 0.0, + 0.0, + -0.3, + -0.1, + 0.0, + 0.0, + 0.1, + -0.1, + 0.0, + 0.0, + 0.1, + 0.0 + ], + "280000": [ + 0.0, + 0.0, + 0.1, + 0.2, + 0.0, + 0.0, + -0.1, + 0.2, + 0.0, + -0.1, + 0.0, + 0.0 + ], + "320000": [ + -0.1, + 0.1, + 0.5, + 0.2, + 0.0, + 0.0, + 0.4, + 0.2, + 0.0, + -0.1, + 0.1, + 0.0 + ], + "360000": [ + 0.0, + 0.2, + -0.1, + 0.1, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "400000": [ + 0.0, + 0.1, + 0.2, + -0.1, + 0.0, + 0.0, + -0.1, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "40000": [ + -0.1, + 0.2, + -0.2, + 0.4, + 0.0, + 0.0, + -0.2, + -0.2, + 0.0, + -0.2, + 0.0, + 0.0 + ], + "440000": [ + 0.0, + 0.1, + 0.0, + 0.1, + 0.0, + 0.0, + 0.0, + -0.2, + 0.0, + 0.2, + 0.0, + 0.0 + ], + "480000": [ + -0.1, + 0.0, + 0.0, + -0.2, + 0.0, + 0.0, + 0.1, + 0.0, + 0.0, + 0.3, + 0.0, + 0.0 + ], + "520000": [ + -0.2, + -0.1, + -0.3, + -0.4, + 0.0, + 0.0, + 0.1, + -0.1, + 0.0, + -0.1, + 0.1, + -0.1 + ], + "560000": [ + 0.2, + -0.2, + 0.1, + 0.1, + 0.0, + 0.0, + 0.1, + 0.0, + 0.0, + -0.1, + 0.0, + 0.0 + ], + "600000": [ + 0.1, + -0.1, + 0.0, + 0.1, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.1, + 0.1, + 0.0 + ], + "640000": [ + 0.1, + 0.0, + -0.3, + 0.2, + 0.0, + 0.0, + 0.1, + -0.1, + 0.0, + -0.4, + 0.0, + 0.0 + ], + "680000": [ + -0.1, + 0.0, + 0.1, + -0.5, + 0.0, + 0.0, + -0.1, + 0.2, + 0.0, + 0.2, + 0.0, + 0.0 + ], + "720000": [ + -0.2, + 0.0, + 0.3, + -0.3, + 0.0, + 0.0, + 0.1, + 0.0, + 0.0, + 0.3, + 0.0, + 0.0 + ], + "760000": [ + 0.1, + -0.2, + 0.0, + 0.1, + 0.0, + 0.0, + -0.2, + 0.1, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "800000": [ + 0.0, + 0.1, + 0.0, + 0.0, + 0.0, + 0.0, + 0.2, + -0.1, + 0.0, + 0.1, + 0.0, + 0.0 + ], + "80000": [ + 0.1, + 0.0, + -0.1, + -0.4, + 0.0, + 0.0, + -0.1, + 0.1, + 0.0, + -0.1, + 0.0, + 0.0 + ], + "840000": [ + 0.1, + 0.2, + 0.1, + 0.3, + 0.0, + 0.0, + 0.2, + 0.4, + 0.0, + 0.1, + 0.0, + 0.0 + ], + "880000": [ + 0.0, + -0.1, + -0.1, + -0.2, + 0.0, + 0.0, + 0.2, + 0.0, + 0.0, + -0.2, + 0.1, + 0.0 + ], + "920000": [ + -0.3, + 0.0, + -0.2, + 0.3, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + -0.4, + 0.0, + 0.0 + ], + "960000": [ + -0.2, + 0.3, + 0.0, + 0.3, + 0.0, + 0.0, + -0.2, + 0.2, + 0.0, + 0.2, + 0.0, + 0.0 + ] + }, + "DR_PFSP": { + "0": [ + 0.2, + 0.2, + -0.1, + -0.4, + 0.0, + 0.0, + -0.2, + 0.1, + 0.0, + 0.2, + 0.0, + 0.0 + ], + "1000000": [ + 0.0, + 0.0, + 0.2, + -0.4, + 0.0, + 0.0, + -0.1, + 0.3, + 0.0, + 0.0, + -0.1, + 0.1 + ], + "1040000": [ + 0.0, + -0.1, + 0.0, + -0.3, + 0.0, + 0.0, + -0.1, + 0.2, + 0.0, + 0.1, + 0.1, + 0.0 + ], + "1080000": [ + 0.2, + 0.2, + 0.0, + 0.3, + 0.0, + 0.0, + -0.2, + -0.5, + 0.0, + 0.0, + 0.1, + 0.0 + ], + "1120000": [ + -0.1, + 0.4, + 0.2, + -0.4, + 0.0, + 0.0, + -0.3, + 0.0, + 0.0, + -0.3, + 0.0, + 0.0 + ], + "1160000": [ + -0.1, + 0.1, + -0.2, + -0.1, + 0.0, + 0.0, + -0.2, + -0.1, + 0.0, + -0.1, + 0.0, + 0.0 + ], + "1200000": [ + 0.1, + -0.2, + 0.3, + 0.2, + 0.0, + 0.0, + -0.1, + 0.0, + 0.0, + 0.2, + 0.0, + 0.0 + ], + "120000": [ + 0.0, + -0.1, + 0.0, + 0.0, + 0.0, + 0.0, + 0.2, + -0.1, + 0.0, + 0.1, + 0.1, + 0.0 + ], + "1240000": [ + -0.1, + 0.1, + 0.0, + -0.1, + 0.0, + 0.0, + -0.1, + 0.0, + 0.0, + -0.2, + 0.2, + 0.0 + ], + "1280000": [ + 0.0, + -0.2, + 0.1, + 0.0, + 0.0, + 0.0, + 0.1, + 0.0, + 0.0, + -0.2, + 0.0, + 0.0 + ], + "1320000": [ + -0.1, + 0.0, + 0.0, + -0.1, + 0.0, + 0.0, + -0.2, + 0.3, + 0.0, + -0.2, + 0.2, + 0.0 + ], + "1360000": [ + 0.0, + 0.0, + 0.1, + -0.1, + 0.0, + 0.0, + 0.0, + 0.3, + 0.0, + -0.1, + 0.0, + 0.0 + ], + "1400000": [ + 0.1, + 0.1, + 0.0, + 0.6, + 0.0, + 0.0, + -0.1, + -0.1, + 0.0, + 0.2, + 0.0, + 0.0 + ], + "1440000": [ + -0.1, + 0.1, + -0.2, + 0.1, + 0.0, + 0.0, + 0.0, + -0.1, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "1480000": [ + 0.1, + -0.1, + 0.3, + -0.3, + 0.0, + 0.0, + -0.1, + 0.1, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "1520000": [ + 0.0, + 0.0, + -0.1, + 0.6, + 0.0, + 0.0, + -0.3, + 0.0, + 0.0, + 0.3, + -0.1, + 0.0 + ], + "1560000": [ + -0.1, + 0.1, + 0.0, + -0.1, + 0.0, + 0.0, + 0.1, + -0.2, + 0.0, + 0.0, + 0.1, + -0.1 + ], + "160000": [ + -0.1, + -0.2, + 0.0, + 0.3, + 0.0, + 0.0, + 0.1, + 0.0, + 0.0, + 0.2, + 0.1, + 0.0 + ], + "200000": [ + 0.3, + 0.1, + 0.1, + 0.2, + 0.0, + 0.0, + 0.1, + -0.2, + 0.0, + 0.1, + 0.2, + 0.0 + ], + "240000": [ + 0.0, + 0.0, + 0.3, + 0.1, + 0.0, + 0.0, + -0.1, + 0.1, + 0.0, + 0.0, + -0.1, + 0.0 + ], + "280000": [ + 0.0, + 0.0, + -0.1, + -0.2, + 0.0, + 0.0, + 0.1, + -0.2, + 0.0, + 0.1, + 0.0, + 0.0 + ], + "320000": [ + 0.1, + -0.1, + -0.5, + -0.2, + 0.0, + 0.0, + -0.4, + -0.2, + 0.0, + 0.1, + -0.1, + 0.0 + ], + "360000": [ + 0.0, + -0.2, + 0.1, + -0.1, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "400000": [ + 0.0, + -0.1, + -0.2, + 0.1, + 0.0, + 0.0, + 0.1, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "40000": [ + 0.1, + -0.2, + 0.2, + -0.4, + 0.0, + 0.0, + 0.2, + 0.2, + 0.0, + 0.2, + 0.0, + 0.0 + ], + "440000": [ + 0.0, + -0.1, + 0.0, + -0.1, + 0.0, + 0.0, + 0.0, + 0.2, + 0.0, + -0.2, + 0.0, + 0.0 + ], + "480000": [ + 0.1, + 0.0, + 0.0, + 0.2, + 0.0, + 0.0, + -0.1, + 0.0, + 0.0, + -0.3, + 0.0, + 0.0 + ], + "520000": [ + 0.2, + 0.1, + 0.3, + 0.4, + 0.0, + 0.0, + -0.1, + 0.1, + 0.0, + 0.1, + -0.1, + 0.1 + ], + "560000": [ + -0.2, + 0.2, + -0.1, + -0.1, + 0.0, + 0.0, + -0.1, + 0.0, + 0.0, + 0.1, + 0.0, + 0.0 + ], + "600000": [ + -0.1, + 0.1, + 0.0, + -0.1, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + -0.1, + -0.1, + 0.0 + ], + "640000": [ + -0.1, + 0.0, + 0.3, + -0.2, + 0.0, + 0.0, + -0.1, + 0.1, + 0.0, + 0.4, + 0.0, + 0.0 + ], + "680000": [ + 0.1, + 0.0, + -0.1, + 0.5, + 0.0, + 0.0, + 0.1, + -0.2, + 0.0, + -0.2, + 0.0, + 0.0 + ], + "720000": [ + 0.2, + 0.0, + -0.3, + 0.3, + 0.0, + 0.0, + -0.1, + 0.0, + 0.0, + -0.3, + 0.0, + 0.0 + ], + "760000": [ + -0.1, + 0.2, + 0.0, + -0.1, + 0.0, + 0.0, + 0.2, + -0.1, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "800000": [ + 0.0, + -0.1, + 0.0, + 0.0, + 0.0, + 0.0, + -0.2, + 0.1, + 0.0, + -0.1, + 0.0, + 0.0 + ], + "80000": [ + -0.1, + 0.0, + 0.1, + 0.4, + 0.0, + 0.0, + 0.1, + -0.1, + 0.0, + 0.1, + 0.0, + 0.0 + ], + "840000": [ + -0.1, + -0.2, + -0.1, + -0.3, + 0.0, + 0.0, + -0.2, + -0.4, + 0.0, + -0.1, + 0.0, + 0.0 + ], + "880000": [ + 0.0, + 0.1, + 0.1, + 0.2, + 0.0, + 0.0, + -0.2, + 0.0, + 0.0, + 0.2, + -0.1, + 0.0 + ], + "920000": [ + 0.3, + 0.0, + 0.2, + -0.3, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.4, + 0.0, + 0.0 + ], + "960000": [ + 0.2, + -0.3, + 0.0, + -0.3, + 0.0, + 0.0, + 0.2, + -0.2, + 0.0, + -0.2, + 0.0, + 0.0 + ] + } +} \ No newline at end of file diff --git a/syllabus/examples/experimental/round_robin/rr_parsing.py b/syllabus/examples/experimental/round_robin/rr_parsing.py new file mode 100644 index 00000000..7b503cfd --- /dev/null +++ b/syllabus/examples/experimental/round_robin/rr_parsing.py @@ -0,0 +1,247 @@ +import argparse +import json +import os +from json import JSONDecodeError + +import matplotlib.pyplot as plt +import pandas as pd +import seaborn as sns +import wandb +from tqdm import tqdm + + +def parse_args(): + parser = argparse.ArgumentParser() + parser.add_argument( + "--logging-dir", + type=str, + default=".", + help="the base directory for logging and wandb storage.", + ) + parser.add_argument( + "--wandb-project-name", + type=str, + default="syllabus", + help="the wandb's project name", + ) + parser.add_argument( + "--wandb-entity", + type=str, + default="rpegoud", + help="the entity (team) of wandb's project", + ) + + args = parser.parse_args() + return args + + +def correct_json_format(path: str, overwrite: bool = False): + """Converts a round-robin output to correct json format.""" + try: + with open(path) as f: + logs = json.load(f) + return logs + + except JSONDecodeError: + agent_path = path.split("/")[0].split("_") + agent_1 = "_".join(agent_path[:2]) + agent_2 = "_".join(agent_path[2:]) + + ckp_path = path.split(".")[0].split("/")[-1].split("_") + ckp_1 = ckp_path[0] + ckp_2 = ckp_path[2] + + with open(path, "r") as file: + lines = file.readlines() + data = [eval(line.strip()) for line in lines] + assert len(data) == 2, "More than two entries" + new_data = { + agent_1: {ckp_1: data[0]}, + agent_2: {ckp_2: data[1]}, + } + + if overwrite: + with open(path, "w") as output_file: + json.dump(new_data, output_file, indent=4) + + return new_data + + +def merge_logs(dir: str, log_dir: str): + """ + Parses the matchup folder and returns a + dictionary of returns for each agent/checkpoint pair + """ + agent_path = dir.split("/")[0].split("_") + agent_1 = "_".join(agent_path[:2]) + agent_2 = "_".join(agent_path[2:]) + + merged_logs = {agent_1: {}, agent_2: {}} + + for file in tqdm(os.listdir(dir)): + path = os.path.join(dir, file).replace(chr(92), "/") + path = path.replace("\\", "/") + try: + with open(os.path.join(dir, file)) as f: + logs = json.load(f) + except JSONDecodeError: + correct_json_format(path, overwrite=True) + with open(os.path.join(dir, file)) as f: + logs = json.load(f) + for agent in [agent_1, agent_2]: + for checkpoint, returns in logs[agent].items(): + if checkpoint not in merged_logs[agent].keys(): + merged_logs[agent][int(checkpoint)] = [] + merged_logs[agent][int(checkpoint)].extend([x for x in returns]) + + with open( + os.path.join(log_dir, f"{agent_1}_{agent_2}_returns.json"), + "w", + ) as output_file: + json.dump(merged_logs, output_file, indent=4) + + return merged_logs + + +def plot_results(data: dict): + + LABELSIZE = 36 + TICKLABELSIZE = 24 + + fig, ax = plt.subplots(figsize=(7 * 2, 3.4 * 2)) + color_palette = "colorblind" + color_palette = sns.color_palette("deep", n_colors=len(data.keys())) + colors = dict(zip(data.keys(), color_palette)) + + for alg in data.keys(): + df = pd.DataFrame(data[alg]) + df.columns = [int(col) for col in df.columns] + df = df.T.sort_index() + avg = df.mean(axis=1) + std = df.std(axis=1) + plt.plot( + df.index, + avg, + color=colors[alg], + marker="o", + linewidth=2, + label=alg, + ) + plt.fill_between( + df.index, + y1=avg + std, + y2=avg - std, + color=colors[alg], + alpha=0.2, + ) + + plt.legend( + loc="lower center", + ncol=len(data.keys()), + bbox_to_anchor=(0.5, 1.0), + fontsize=30, + ) + ax.spines["right"].set_visible(False) + ax.spines["top"].set_visible(False) + ax.spines["left"].set_linewidth(2) + ax.spines["bottom"].set_linewidth(2) + + ax.tick_params(length=0.1, width=0.1, labelsize=TICKLABELSIZE) + ax.spines["left"].set_position(("outward", 10)) + ax.spines["bottom"].set_position(("outward", 10)) + ax.set_xlabel("Steps", fontsize=LABELSIZE) + ax.set_ylabel("Mean Episodic Return", fontsize=LABELSIZE) + fig.tight_layout() + ax.grid(True, alpha=0.2) + + return fig + + +def sort_and_trim_dicts(data: dict) -> dict: + min_length = min(len(sub_dict) for sub_dict in data.values()) + + def sort_and_trim(sub_dict, N): + sorted_keys = sorted(sub_dict.keys(), key=lambda x: int(x))[:N] + return {k: sub_dict[k] for k in sorted_keys} + + trimmed_data = { + key: sort_and_trim(sub_dict, min_length) for key, sub_dict in data.items() + } + + return trimmed_data + + +def check_dir_name(dir: str, legal_names: list) -> bool: + split = dir.split("_") + return all([x in legal_names for x in split]) + + +if __name__ == "__main__": + args = parse_args() + legal_names = ("DR", "PLR", "SP", "FSP", "PFSP") + legal_combinations = ("DR_SP", "DR_FSP", "DR_PFSP", "PLR_SP", "PLR_FSP", "PLR_PFSP") + valid_dirs = [dir for dir in os.listdir() if check_dir_name(dir, legal_names)] + print(f"valid directories: {valid_dirs}") + + os.makedirs(args.logging_dir, exist_ok=True) + + # parse the round-robin results, convert to json and save to logging dir + print("formatting round-robin results ...") + for dir in valid_dirs: + print(dir) + agent_path = dir.split("_") + agent_1 = "_".join(agent_path[:2]) + agent_2 = "_".join(agent_path[2:]) + + for agent in [agent_1, agent_2]: + assert ( + agent in legal_combinations + ), f"Expected agent name in {legal_combinations} but got {agent}" + + logs = merge_logs(dir, args.logging_dir) + + # merge all the logs in logging dir and plot the result + print("merging results ...") + df = {} + logs = [file for file in os.listdir(args.logging_dir) if ".json" in file] + for log in logs: + print(log) + with open(os.path.join(args.logging_dir, log), "r") as f: + returns = json.load(f) + + agent_1, agent_2 = list(returns.keys()) + if agent_1 not in df.keys(): + df[agent_1] = returns[agent_1] + if agent_2 not in df.keys(): + df[agent_2] = returns[agent_2] + else: + for checkpoint in list(returns[agent_1].keys()): + df[agent_1][checkpoint].extend(returns[agent_1][checkpoint]) + for checkpoint in list(returns[agent_2].keys()): + df[agent_2][checkpoint].extend(returns[agent_2][checkpoint]) + + with open( + os.path.join(args.logging_dir, "merged_rr_returns.json"), + "w", + ) as output_file: + json.dump(df, output_file, indent=4) + + fig = plot_results(df) + fig.savefig(f"{args.logging_dir}/rr_returns.png", bbox_inches="tight") + + print("logging ...") + wandb.init( + project=args.wandb_project_name, + entity=args.wandb_entity, + sync_tensorboard=True, + config=vars(args), + name="round_robin_results", + save_code=True, + dir=args.logging_dir, + ) + + wandb.run.log_code(os.path.join(args.logging_dir)) + returns_artifact = wandb.Artifact("returns", type="json") + returns_artifact.add_file(os.path.join(args.logging_dir, "merged_rr_returns.json")) + wandb.log_artifact(returns_artifact) + wandb.log({"charts/rr_returns": wandb.Image(fig)}) diff --git a/syllabus/examples/experimental/round_robin/rr_viz.ipynb b/syllabus/examples/experimental/round_robin/rr_viz.ipynb new file mode 100644 index 00000000..cf7a2297 --- /dev/null +++ b/syllabus/examples/experimental/round_robin/rr_viz.ipynb @@ -0,0 +1,9636 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 55, + "metadata": {}, + "outputs": [], + "source": [ + "import json\n", + "import os\n", + "from json import JSONDecodeError\n", + "import sys\n", + "import numpy as np\n", + "import pandas as pd\n", + "import plotly\n", + "import plotly.graph_objects as go\n", + "import matplotlib.pyplot as plt\n", + "import seaborn as sns" + ] + }, + { + "cell_type": "code", + "execution_count": 56, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "['DR_FSP_DR_PFSP',\n", + " 'DR_SP_DR_FSP',\n", + " 'DR_SP_DR_PFSP',\n", + " 'parse_results',\n", + " 'rr_parsing.py',\n", + " 'rr_viz.ipynb']" + ] + }, + "execution_count": 56, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "os.listdir()" + ] + }, + { + "cell_type": "code", + "execution_count": 57, + "metadata": {}, + "outputs": [], + "source": [ + "def plot_returns(values: pd.Series, agent_name: str):\n", + " df = pd.DataFrame(values)\n", + " df.columns = [\"values\"]\n", + " df[\"avg\"] = df[\"values\"].apply(lambda x: np.array(x).mean())\n", + " df[\"std\"] = df[\"values\"].apply(lambda x: np.array(x).std())\n", + "\n", + " df[\"lower\"] = df[\"avg\"] - df[\"std\"]\n", + " df[\"upper\"] = df[\"avg\"] + df[\"std\"]\n", + "\n", + " fig = go.Figure()\n", + "\n", + " fig.add_trace(\n", + " go.Scatter(\n", + " x=np.array([df.index, df.index[::-1]]).flatten(),\n", + " y=np.array([df[\"upper\"], df[\"lower\"][::-1]]).flatten(),\n", + " fill=\"toself\",\n", + " fillcolor=\"rgba(0, 0, 255, 0.2)\",\n", + " line=dict(color=\"rgba(255, 255, 255, 0)\"),\n", + " name=\"Error Band\",\n", + " )\n", + " )\n", + " fig.add_trace(\n", + " go.Scatter(\n", + " x=df.index,\n", + " y=df[\"avg\"],\n", + " mode=\"lines\",\n", + " line=dict(color=\"blue\"),\n", + " name=\"Average\",\n", + " )\n", + " )\n", + "\n", + " fig.update_layout(\n", + " title=agent_name,\n", + " xaxis_title=\"Checkpoints\",\n", + " yaxis_title=\"Returns\",\n", + " showlegend=True,\n", + " )\n", + " fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 58, + "metadata": {}, + "outputs": [], + "source": [ + "def plot_logs(agent_1: str, agent_2: str):\n", + " with open(f\"parse_results/{agent_1}_{agent_2}_returns.json\", \"r\") as f:\n", + " logs = json.load(f)\n", + " plot_returns(pd.Series(logs[agent_1]), agent_1)\n", + " plot_returns(pd.Series(logs[agent_2]), agent_2)" + ] + }, + { + "cell_type": "code", + "execution_count": 59, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.plotly.v1+json": { + "config": { + "plotlyServerURL": "https://plotly.com" + }, + "data": [ + { + "fill": "toself", + "fillcolor": "rgba(0, 0, 255, 0.2)", + "line": { + "color": "rgba(255, 255, 255, 0)" + }, + "name": "Error Band", + "type": "scatter", + "x": [ + "0", + "1000000", + "1040000", + "1080000", + "1120000", + "1160000", + "1200000", + "120000", + "1240000", + "1280000", + "1320000", + "1360000", + "1400000", + "1440000", + "1480000", + "1520000", + "1560000", + "160000", + "200000", + "240000", + "280000", + "320000", + "360000", + "400000", + "40000", + "440000", + "480000", + "520000", + "560000", + "600000", + "640000", + "680000", + "720000", + "760000", + "800000", + "80000", + "840000", + "880000", + "920000", + "960000", + "960000", + "920000", + "880000", + "840000", + "80000", + "800000", + "760000", + "720000", + "680000", + "640000", + "600000", + "560000", + "520000", + "480000", + "440000", + "40000", + "400000", + "360000", + "320000", + "280000", + "240000", + "200000", + "160000", + "1560000", + "1520000", + "1480000", + "1440000", + "1400000", + "1360000", + "1320000", + "1280000", + "1240000", + "120000", + "1200000", + "1160000", + "1120000", + "1080000", + "1040000", + "1000000", + "0" + ], + "y": [ + 0.15315906800216375, + 0.03704602618549665, + 0.037046026185496635, + 0.09035816264795715, + 0.12122697492492843, + 0.07786733693990501, + 0.1337713240271471, + 0.04096733152583014, + 0.11039839040645842, + 0.09541582998323946, + 0.18273733406281564, + 0.16702864369671522, + 0.15490381056766578, + 0.11770429580497581, + 0.1152679963849936, + 0.20410352085392205, + 0.06326385872187865, + 0.1, + 0.1260362971081845, + 0.09759424334001739, + 0.09659719205521199, + 0.1152679963849936, + 0.12376916288627263, + 0.06902380714238082, + 0.18257418583505536, + 0.05165032522654641, + 0.06982573466883041, + 0.11982573466883044, + 0.12376916288627265, + 0.09136866703140781, + 0.04716878364870321, + 0.1024301372149215, + 0.0912870929175277, + 0.10371269285216331, + 0.0979001312335302, + 0.16116778865306824, + 0.08704602618549664, + 0.1260362971081845, + 0.11583123951777, + 0.08164965809277261, + -0.08164965809277261, + -0.21583123951777003, + -0.07603629710818449, + -0.10371269285216331, + -0.11116778865306824, + -0.16456679790019685, + -0.08704602618549664, + -0.0912870929175277, + -0.18576347054825487, + -0.09716878364870322, + -0.14136866703140782, + -0.15710249621960598, + -0.10315906800216378, + -0.15315906800216375, + -0.11831699189321307, + -0.18257418583505536, + -0.16902380714238083, + -0.15710249621960595, + -0.08193466305166028, + -0.06326385872187866, + -0.13092757667335073, + -0.0760362971081845, + -0.1, + -0.096597192055212, + -0.0707701875205887, + -0.08193466305166028, + -0.15103762913830915, + -0.10490381056766579, + -0.2170286436967152, + -0.28273733406281565, + -0.11208249664990612, + -0.12706505707312507, + -0.0576339981924968, + -0.18377132402714708, + -0.09453400360657167, + -0.18789364159159508, + -0.17369149598129047, + -0.15371269285216332, + -0.1537126928521633, + -0.06982573466883044 + ] + }, + { + "line": { + "color": "blue" + }, + "mode": "lines", + "name": "Average", + "type": "scatter", + "x": [ + "0", + "1000000", + "1040000", + "1080000", + "1120000", + "1160000", + "1200000", + "120000", + "1240000", + "1280000", + "1320000", + "1360000", + "1400000", + "1440000", + "1480000", + "1520000", + "1560000", + "160000", + "200000", + "240000", + "280000", + "320000", + "360000", + "400000", + "40000", + "440000", + "480000", + "520000", + "560000", + "600000", + "640000", + "680000", + "720000", + "760000", + "800000", + "80000", + "840000", + "880000", + "920000", + "960000" + ], + "y": [ + 0.041666666666666664, + -0.05833333333333333, + -0.05833333333333334, + -0.041666666666666664, + -0.03333333333333333, + -0.008333333333333333, + -0.025000000000000005, + -0.008333333333333333, + -0.00833333333333333, + -0.008333333333333337, + -0.049999999999999996, + -0.024999999999999998, + 0.024999999999999998, + -0.016666666666666677, + 0.016666666666666666, + 0.06666666666666668, + -0.01666666666666667, + 0, + 0.025000000000000005, + -0.016666666666666666, + 0.016666666666666666, + 0.016666666666666666, + -0.016666666666666666, + -0.05000000000000001, + 0, + -0.03333333333333333, + -0.041666666666666664, + 0.008333333333333333, + -0.01666666666666667, + -0.025000000000000005, + -0.025000000000000005, + -0.04166666666666668, + -2.3129646346357427e-18, + 0.008333333333333333, + -0.03333333333333333, + 0.025000000000000005, + -0.008333333333333333, + 0.025000000000000005, + -0.05000000000000001, + 0 + ] + } + ], + "layout": { + "showlegend": true, + "template": { + "data": { + "bar": [ + { + "error_x": { + "color": "#2a3f5f" + }, + "error_y": { + "color": "#2a3f5f" + }, + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "bar" + } + ], + "barpolar": [ + { + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "barpolar" + } + ], + "carpet": [ + { + "aaxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "baxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "type": "carpet" + } + ], + "choropleth": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "choropleth" + } + ], + "contour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "contour" + } + ], + "contourcarpet": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "contourcarpet" + } + ], + "heatmap": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "heatmap" + } + ], + "heatmapgl": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "heatmapgl" + } + ], + "histogram": [ + { + "marker": { + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "histogram" + } + ], + "histogram2d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "histogram2d" + } + ], + "histogram2dcontour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "histogram2dcontour" + } + ], + "mesh3d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "mesh3d" + } + ], + "parcoords": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "parcoords" + } + ], + "pie": [ + { + "automargin": true, + "type": "pie" + } + ], + "scatter": [ + { + "fillpattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + }, + "type": "scatter" + } + ], + "scatter3d": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatter3d" + } + ], + "scattercarpet": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattercarpet" + } + ], + "scattergeo": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergeo" + } + ], + "scattergl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergl" + } + ], + "scattermapbox": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattermapbox" + } + ], + "scatterpolar": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolar" + } + ], + "scatterpolargl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolargl" + } + ], + "scatterternary": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterternary" + } + ], + "surface": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "surface" + } + ], + "table": [ + { + "cells": { + "fill": { + "color": "#EBF0F8" + }, + "line": { + "color": "white" + } + }, + "header": { + "fill": { + "color": "#C8D4E3" + }, + "line": { + "color": "white" + } + }, + "type": "table" + } + ] + }, + "layout": { + "annotationdefaults": { + "arrowcolor": "#2a3f5f", + "arrowhead": 0, + "arrowwidth": 1 + }, + "autotypenumbers": "strict", + "coloraxis": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "colorscale": { + "diverging": [ + [ + 0, + "#8e0152" + ], + [ + 0.1, + "#c51b7d" + ], + [ + 0.2, + "#de77ae" + ], + [ + 0.3, + "#f1b6da" + ], + [ + 0.4, + "#fde0ef" + ], + [ + 0.5, + "#f7f7f7" + ], + [ + 0.6, + "#e6f5d0" + ], + [ + 0.7, + "#b8e186" + ], + [ + 0.8, + "#7fbc41" + ], + [ + 0.9, + "#4d9221" + ], + [ + 1, + "#276419" + ] + ], + "sequential": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "sequentialminus": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ] + }, + "colorway": [ + "#636efa", + "#EF553B", + "#00cc96", + "#ab63fa", + "#FFA15A", + "#19d3f3", + "#FF6692", + "#B6E880", + "#FF97FF", + "#FECB52" + ], + "font": { + "color": "#2a3f5f" + }, + "geo": { + "bgcolor": "white", + "lakecolor": "white", + "landcolor": "#E5ECF6", + "showlakes": true, + "showland": true, + "subunitcolor": "white" + }, + "hoverlabel": { + "align": "left" + }, + "hovermode": "closest", + "mapbox": { + "style": "light" + }, + "paper_bgcolor": "white", + "plot_bgcolor": "#E5ECF6", + "polar": { + "angularaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "radialaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "scene": { + "xaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "yaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "zaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + } + }, + "shapedefaults": { + "line": { + "color": "#2a3f5f" + } + }, + "ternary": { + "aaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "baxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "caxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "title": { + "x": 0.05 + }, + "xaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + }, + "yaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + } + } + }, + "title": { + "text": "DR_SP" + }, + "xaxis": { + "title": { + "text": "Checkpoints" + } + }, + "yaxis": { + "title": { + "text": "Returns" + } + } + } + } + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.plotly.v1+json": { + "config": { + "plotlyServerURL": "https://plotly.com" + }, + "data": [ + { + "fill": "toself", + "fillcolor": "rgba(0, 0, 255, 0.2)", + "line": { + "color": "rgba(255, 255, 255, 0)" + }, + "name": "Error Band", + "type": "scatter", + "x": [ + "0", + "1000000", + "1040000", + "1080000", + "1120000", + "1160000", + "1200000", + "120000", + "1240000", + "1280000", + "1320000", + "1360000", + "1400000", + "1440000", + "1480000", + "1520000", + "1560000", + "160000", + "200000", + "240000", + "280000", + "320000", + "360000", + "400000", + "40000", + "440000", + "480000", + "520000", + "560000", + "600000", + "640000", + "680000", + "720000", + "760000", + "800000", + "80000", + "840000", + "880000", + "920000", + "960000", + "960000", + "920000", + "880000", + "840000", + "80000", + "800000", + "760000", + "720000", + "680000", + "640000", + "600000", + "560000", + "520000", + "480000", + "440000", + "40000", + "400000", + "360000", + "320000", + "280000", + "240000", + "200000", + "160000", + "1560000", + "1520000", + "1480000", + "1440000", + "1400000", + "1360000", + "1320000", + "1280000", + "1240000", + "120000", + "1200000", + "1160000", + "1120000", + "1080000", + "1040000", + "1000000", + "0" + ], + "y": [ + 0.06982573466883044, + 0.1537126928521633, + 0.15371269285216332, + 0.17369149598129047, + 0.18789364159159508, + 0.09453400360657167, + 0.18377132402714708, + 0.0576339981924968, + 0.12706505707312507, + 0.11208249664990612, + 0.28273733406281565, + 0.2170286436967152, + 0.10490381056766579, + 0.15103762913830915, + 0.08193466305166028, + 0.0707701875205887, + 0.096597192055212, + 0.1, + 0.0760362971081845, + 0.13092757667335073, + 0.06326385872187866, + 0.08193466305166028, + 0.15710249621960595, + 0.16902380714238083, + 0.18257418583505536, + 0.11831699189321307, + 0.15315906800216375, + 0.10315906800216378, + 0.15710249621960598, + 0.14136866703140782, + 0.09716878364870322, + 0.18576347054825487, + 0.0912870929175277, + 0.08704602618549664, + 0.16456679790019685, + 0.11116778865306824, + 0.10371269285216331, + 0.07603629710818449, + 0.21583123951777003, + 0.08164965809277261, + -0.08164965809277261, + -0.11583123951777, + -0.1260362971081845, + -0.08704602618549664, + -0.16116778865306824, + -0.0979001312335302, + -0.10371269285216331, + -0.0912870929175277, + -0.1024301372149215, + -0.04716878364870321, + -0.09136866703140781, + -0.12376916288627265, + -0.11982573466883044, + -0.06982573466883041, + -0.05165032522654641, + -0.18257418583505536, + -0.06902380714238082, + -0.12376916288627263, + -0.1152679963849936, + -0.09659719205521199, + -0.09759424334001739, + -0.1260362971081845, + -0.1, + -0.06326385872187865, + -0.20410352085392205, + -0.1152679963849936, + -0.11770429580497581, + -0.15490381056766578, + -0.16702864369671522, + -0.18273733406281564, + -0.09541582998323946, + -0.11039839040645842, + -0.04096733152583014, + -0.1337713240271471, + -0.07786733693990501, + -0.12122697492492843, + -0.09035816264795715, + -0.037046026185496635, + -0.03704602618549665, + -0.15315906800216375 + ] + }, + { + "line": { + "color": "blue" + }, + "mode": "lines", + "name": "Average", + "type": "scatter", + "x": [ + "0", + "1000000", + "1040000", + "1080000", + "1120000", + "1160000", + "1200000", + "120000", + "1240000", + "1280000", + "1320000", + "1360000", + "1400000", + "1440000", + "1480000", + "1520000", + "1560000", + "160000", + "200000", + "240000", + "280000", + "320000", + "360000", + "400000", + "40000", + "440000", + "480000", + "520000", + "560000", + "600000", + "640000", + "680000", + "720000", + "760000", + "800000", + "80000", + "840000", + "880000", + "920000", + "960000" + ], + "y": [ + -0.041666666666666664, + 0.05833333333333333, + 0.05833333333333334, + 0.041666666666666664, + 0.03333333333333333, + 0.008333333333333333, + 0.025000000000000005, + 0.008333333333333333, + 0.00833333333333333, + 0.008333333333333337, + 0.049999999999999996, + 0.024999999999999998, + -0.024999999999999998, + 0.016666666666666677, + -0.016666666666666666, + -0.06666666666666668, + 0.01666666666666667, + 0, + -0.025000000000000005, + 0.016666666666666666, + -0.016666666666666666, + -0.016666666666666666, + 0.016666666666666666, + 0.05000000000000001, + 0, + 0.03333333333333333, + 0.041666666666666664, + -0.008333333333333333, + 0.01666666666666667, + 0.025000000000000005, + 0.025000000000000005, + 0.04166666666666668, + 2.3129646346357427e-18, + -0.008333333333333333, + 0.03333333333333333, + -0.025000000000000005, + 0.008333333333333333, + -0.025000000000000005, + 0.05000000000000001, + 0 + ] + } + ], + "layout": { + "showlegend": true, + "template": { + "data": { + "bar": [ + { + "error_x": { + "color": "#2a3f5f" + }, + "error_y": { + "color": "#2a3f5f" + }, + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "bar" + } + ], + "barpolar": [ + { + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "barpolar" + } + ], + "carpet": [ + { + "aaxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "baxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "type": "carpet" + } + ], + "choropleth": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "choropleth" + } + ], + "contour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "contour" + } + ], + "contourcarpet": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "contourcarpet" + } + ], + "heatmap": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "heatmap" + } + ], + "heatmapgl": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "heatmapgl" + } + ], + "histogram": [ + { + "marker": { + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "histogram" + } + ], + "histogram2d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "histogram2d" + } + ], + "histogram2dcontour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "histogram2dcontour" + } + ], + "mesh3d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "mesh3d" + } + ], + "parcoords": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "parcoords" + } + ], + "pie": [ + { + "automargin": true, + "type": "pie" + } + ], + "scatter": [ + { + "fillpattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + }, + "type": "scatter" + } + ], + "scatter3d": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatter3d" + } + ], + "scattercarpet": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattercarpet" + } + ], + "scattergeo": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergeo" + } + ], + "scattergl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergl" + } + ], + "scattermapbox": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattermapbox" + } + ], + "scatterpolar": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolar" + } + ], + "scatterpolargl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolargl" + } + ], + "scatterternary": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterternary" + } + ], + "surface": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "surface" + } + ], + "table": [ + { + "cells": { + "fill": { + "color": "#EBF0F8" + }, + "line": { + "color": "white" + } + }, + "header": { + "fill": { + "color": "#C8D4E3" + }, + "line": { + "color": "white" + } + }, + "type": "table" + } + ] + }, + "layout": { + "annotationdefaults": { + "arrowcolor": "#2a3f5f", + "arrowhead": 0, + "arrowwidth": 1 + }, + "autotypenumbers": "strict", + "coloraxis": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "colorscale": { + "diverging": [ + [ + 0, + "#8e0152" + ], + [ + 0.1, + "#c51b7d" + ], + [ + 0.2, + "#de77ae" + ], + [ + 0.3, + "#f1b6da" + ], + [ + 0.4, + "#fde0ef" + ], + [ + 0.5, + "#f7f7f7" + ], + [ + 0.6, + "#e6f5d0" + ], + [ + 0.7, + "#b8e186" + ], + [ + 0.8, + "#7fbc41" + ], + [ + 0.9, + "#4d9221" + ], + [ + 1, + "#276419" + ] + ], + "sequential": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "sequentialminus": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ] + }, + "colorway": [ + "#636efa", + "#EF553B", + "#00cc96", + "#ab63fa", + "#FFA15A", + "#19d3f3", + "#FF6692", + "#B6E880", + "#FF97FF", + "#FECB52" + ], + "font": { + "color": "#2a3f5f" + }, + "geo": { + "bgcolor": "white", + "lakecolor": "white", + "landcolor": "#E5ECF6", + "showlakes": true, + "showland": true, + "subunitcolor": "white" + }, + "hoverlabel": { + "align": "left" + }, + "hovermode": "closest", + "mapbox": { + "style": "light" + }, + "paper_bgcolor": "white", + "plot_bgcolor": "#E5ECF6", + "polar": { + "angularaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "radialaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "scene": { + "xaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "yaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "zaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + } + }, + "shapedefaults": { + "line": { + "color": "#2a3f5f" + } + }, + "ternary": { + "aaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "baxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "caxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "title": { + "x": 0.05 + }, + "xaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + }, + "yaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + } + } + }, + "title": { + "text": "DR_FSP" + }, + "xaxis": { + "title": { + "text": "Checkpoints" + } + }, + "yaxis": { + "title": { + "text": "Returns" + } + } + } + } + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "plot_logs(\"DR_SP\", \"DR_FSP\")" + ] + }, + { + "cell_type": "code", + "execution_count": 60, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.plotly.v1+json": { + "config": { + "plotlyServerURL": "https://plotly.com" + }, + "data": [ + { + "fill": "toself", + "fillcolor": "rgba(0, 0, 255, 0.2)", + "line": { + "color": "rgba(255, 255, 255, 0)" + }, + "name": "Error Band", + "type": "scatter", + "x": [ + "0", + "1000000", + "1040000", + "1080000", + "1120000", + "1160000", + "1200000", + "120000", + "1240000", + "1280000", + "1320000", + "1360000", + "1400000", + "1440000", + "1480000", + "1520000", + "1560000", + "160000", + "200000", + "240000", + "280000", + "320000", + "360000", + "400000", + "40000", + "440000", + "480000", + "520000", + "560000", + "600000", + "640000", + "680000", + "720000", + "760000", + "800000", + "80000", + "840000", + "880000", + "920000", + "960000", + "960000", + "920000", + "880000", + "840000", + "80000", + "800000", + "760000", + "720000", + "680000", + "640000", + "600000", + "560000", + "520000", + "480000", + "440000", + "40000", + "400000", + "360000", + "320000", + "280000", + "240000", + "200000", + "160000", + "1560000", + "1520000", + "1480000", + "1440000", + "1400000", + "1360000", + "1320000", + "1280000", + "1240000", + "120000", + "1200000", + "1160000", + "1120000", + "1080000", + "1040000", + "1000000", + "0" + ], + "y": [ + 0.16832508230603466, + 0.16329931618554522, + 0.12706505707312507, + 0.18939684196174453, + 0.2516600528059026, + 0.14453400360657168, + 0.09035816264795715, + 0.06326385872187867, + 0.1152679963849936, + 0.10641941345224173, + 0.146526032931475, + 0.08193466305166028, + 0.11283882690448345, + 0.096597192055212, + 0.13540064007726602, + 0.1801041412477616, + 0.10641941345224173, + 0.09138857955913139, + 0.048322071557906174, + 0.07603629710818449, + 0.11742113755341183, + 0.28841818987478596, + 0.08538509376029435, + 0.08425361315953582, + 0.14880544678845176, + 0.10641941345224173, + 0.11982573466883045, + 0.05243013721492151, + 0.1152679963849936, + 0.08451190357119041, + 0.12652771744375735, + 0.15573467387981, + 0.17911323908014937, + 0.08704602618549664, + 0.09716878364870322, + 0.08388765977766136, + 0.24468576246447685, + 0.08397247358851682, + 0.12078251276599328, + 0.2107275126832159, + -0.11072751268321593, + -0.2207825127659933, + -0.13397247358851683, + -0.011352429131143454, + -0.16722099311099467, + -0.04716878364870321, + -0.10371269285216331, + -0.14577990574681604, + -0.18906800721314332, + -0.193194384110424, + -0.03451190357119041, + -0.08193466305166028, + -0.23576347054825486, + -0.10315906800216379, + -0.07308608011890841, + -0.19880544678845175, + -0.06758694649286916, + -0.052051760426961025, + -0.07175152320811926, + -0.06742113755341181, + -0.1260362971081845, + -0.1983220715579062, + -0.15805524622579803, + -0.07308608011890841, + -0.24677080791442826, + -0.13540064007726602, + -0.06326385872187867, + -0.2461721602378168, + -0.1152679963849936, + -0.12985936626480835, + -0.07308608011890841, + -0.08193466305166028, + -0.096597192055212, + -0.17369149598129047, + -0.027867336939905, + -0.16832671947256925, + -0.2060635086284112, + -0.11039839040645841, + -0.16329931618554522, + -0.16832508230603466 + ] + }, + { + "line": { + "color": "blue" + }, + "mode": "lines", + "name": "Average", + "type": "scatter", + "x": [ + "0", + "1000000", + "1040000", + "1080000", + "1120000", + "1160000", + "1200000", + "120000", + "1240000", + "1280000", + "1320000", + "1360000", + "1400000", + "1440000", + "1480000", + "1520000", + "1560000", + "160000", + "200000", + "240000", + "280000", + "320000", + "360000", + "400000", + "40000", + "440000", + "480000", + "520000", + "560000", + "600000", + "640000", + "680000", + "720000", + "760000", + "800000", + "80000", + "840000", + "880000", + "920000", + "960000" + ], + "y": [ + -2.3129646346357427e-18, + 2.3129646346357427e-18, + 0.008333333333333337, + -0.008333333333333333, + 0.041666666666666664, + 0.05833333333333334, + -0.041666666666666664, + -0.016666666666666666, + 0.016666666666666666, + 0.016666666666666666, + 0.008333333333333337, + -0.016666666666666666, + -0.06666666666666667, + 0.016666666666666666, + 0, + -0.03333333333333333, + 0.016666666666666666, + -0.033333333333333326, + -0.07500000000000001, + -0.025000000000000005, + 0.025000000000000005, + 0.10833333333333334, + 0.016666666666666666, + 0.008333333333333333, + -0.024999999999999998, + 0.016666666666666666, + 0.00833333333333333, + -0.09166666666666667, + 0.01666666666666667, + 0.025000000000000005, + -0.03333333333333333, + -0.016666666666666666, + 0.016666666666666666, + -0.008333333333333333, + 0.025000000000000005, + -0.041666666666666664, + 0.1166666666666667, + -0.025000000000000005, + -0.05000000000000001, + 0.049999999999999996 + ] + } + ], + "layout": { + "showlegend": true, + "template": { + "data": { + "bar": [ + { + "error_x": { + "color": "#2a3f5f" + }, + "error_y": { + "color": "#2a3f5f" + }, + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "bar" + } + ], + "barpolar": [ + { + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "barpolar" + } + ], + "carpet": [ + { + "aaxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "baxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "type": "carpet" + } + ], + "choropleth": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "choropleth" + } + ], + "contour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "contour" + } + ], + "contourcarpet": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "contourcarpet" + } + ], + "heatmap": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "heatmap" + } + ], + "heatmapgl": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "heatmapgl" + } + ], + "histogram": [ + { + "marker": { + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "histogram" + } + ], + "histogram2d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "histogram2d" + } + ], + "histogram2dcontour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "histogram2dcontour" + } + ], + "mesh3d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "mesh3d" + } + ], + "parcoords": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "parcoords" + } + ], + "pie": [ + { + "automargin": true, + "type": "pie" + } + ], + "scatter": [ + { + "fillpattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + }, + "type": "scatter" + } + ], + "scatter3d": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatter3d" + } + ], + "scattercarpet": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattercarpet" + } + ], + "scattergeo": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergeo" + } + ], + "scattergl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergl" + } + ], + "scattermapbox": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattermapbox" + } + ], + "scatterpolar": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolar" + } + ], + "scatterpolargl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolargl" + } + ], + "scatterternary": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterternary" + } + ], + "surface": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "surface" + } + ], + "table": [ + { + "cells": { + "fill": { + "color": "#EBF0F8" + }, + "line": { + "color": "white" + } + }, + "header": { + "fill": { + "color": "#C8D4E3" + }, + "line": { + "color": "white" + } + }, + "type": "table" + } + ] + }, + "layout": { + "annotationdefaults": { + "arrowcolor": "#2a3f5f", + "arrowhead": 0, + "arrowwidth": 1 + }, + "autotypenumbers": "strict", + "coloraxis": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "colorscale": { + "diverging": [ + [ + 0, + "#8e0152" + ], + [ + 0.1, + "#c51b7d" + ], + [ + 0.2, + "#de77ae" + ], + [ + 0.3, + "#f1b6da" + ], + [ + 0.4, + "#fde0ef" + ], + [ + 0.5, + "#f7f7f7" + ], + [ + 0.6, + "#e6f5d0" + ], + [ + 0.7, + "#b8e186" + ], + [ + 0.8, + "#7fbc41" + ], + [ + 0.9, + "#4d9221" + ], + [ + 1, + "#276419" + ] + ], + "sequential": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "sequentialminus": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ] + }, + "colorway": [ + "#636efa", + "#EF553B", + "#00cc96", + "#ab63fa", + "#FFA15A", + "#19d3f3", + "#FF6692", + "#B6E880", + "#FF97FF", + "#FECB52" + ], + "font": { + "color": "#2a3f5f" + }, + "geo": { + "bgcolor": "white", + "lakecolor": "white", + "landcolor": "#E5ECF6", + "showlakes": true, + "showland": true, + "subunitcolor": "white" + }, + "hoverlabel": { + "align": "left" + }, + "hovermode": "closest", + "mapbox": { + "style": "light" + }, + "paper_bgcolor": "white", + "plot_bgcolor": "#E5ECF6", + "polar": { + "angularaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "radialaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "scene": { + "xaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "yaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "zaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + } + }, + "shapedefaults": { + "line": { + "color": "#2a3f5f" + } + }, + "ternary": { + "aaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "baxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "caxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "title": { + "x": 0.05 + }, + "xaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + }, + "yaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + } + } + }, + "title": { + "text": "DR_SP" + }, + "xaxis": { + "title": { + "text": "Checkpoints" + } + }, + "yaxis": { + "title": { + "text": "Returns" + } + } + } + } + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.plotly.v1+json": { + "config": { + "plotlyServerURL": "https://plotly.com" + }, + "data": [ + { + "fill": "toself", + "fillcolor": "rgba(0, 0, 255, 0.2)", + "line": { + "color": "rgba(255, 255, 255, 0)" + }, + "name": "Error Band", + "type": "scatter", + "x": [ + "0", + "1000000", + "1040000", + "1080000", + "1120000", + "1160000", + "1200000", + "120000", + "1240000", + "1280000", + "1320000", + "1360000", + "1400000", + "1440000", + "1480000", + "1520000", + "1560000", + "160000", + "200000", + "240000", + "280000", + "320000", + "360000", + "400000", + "40000", + "440000", + "480000", + "520000", + "560000", + "600000", + "640000", + "680000", + "720000", + "760000", + "800000", + "80000", + "840000", + "880000", + "920000", + "960000", + "960000", + "920000", + "880000", + "840000", + "80000", + "800000", + "760000", + "720000", + "680000", + "640000", + "600000", + "560000", + "520000", + "480000", + "440000", + "40000", + "400000", + "360000", + "320000", + "280000", + "240000", + "200000", + "160000", + "1560000", + "1520000", + "1480000", + "1440000", + "1400000", + "1360000", + "1320000", + "1280000", + "1240000", + "120000", + "1200000", + "1160000", + "1120000", + "1080000", + "1040000", + "1000000", + "0" + ], + "y": [ + 0.16832508230603466, + 0.16329931618554522, + 0.11039839040645841, + 0.2060635086284112, + 0.16832671947256925, + 0.027867336939905, + 0.17369149598129047, + 0.096597192055212, + 0.08193466305166028, + 0.07308608011890841, + 0.12985936626480835, + 0.1152679963849936, + 0.2461721602378168, + 0.06326385872187867, + 0.13540064007726602, + 0.24677080791442826, + 0.07308608011890841, + 0.15805524622579803, + 0.1983220715579062, + 0.1260362971081845, + 0.06742113755341181, + 0.07175152320811926, + 0.052051760426961025, + 0.06758694649286916, + 0.19880544678845175, + 0.07308608011890841, + 0.10315906800216379, + 0.23576347054825486, + 0.08193466305166028, + 0.03451190357119041, + 0.193194384110424, + 0.18906800721314332, + 0.14577990574681604, + 0.10371269285216331, + 0.04716878364870321, + 0.16722099311099467, + 0.011352429131143454, + 0.13397247358851683, + 0.2207825127659933, + 0.11072751268321593, + -0.2107275126832159, + -0.12078251276599328, + -0.08397247358851682, + -0.24468576246447685, + -0.08388765977766136, + -0.09716878364870322, + -0.08704602618549664, + -0.17911323908014937, + -0.15573467387981, + -0.12652771744375735, + -0.08451190357119041, + -0.1152679963849936, + -0.05243013721492151, + -0.11982573466883045, + -0.10641941345224173, + -0.14880544678845176, + -0.08425361315953582, + -0.08538509376029435, + -0.28841818987478596, + -0.11742113755341183, + -0.07603629710818449, + -0.048322071557906174, + -0.09138857955913139, + -0.10641941345224173, + -0.1801041412477616, + -0.13540064007726602, + -0.096597192055212, + -0.11283882690448345, + -0.08193466305166028, + -0.146526032931475, + -0.10641941345224173, + -0.1152679963849936, + -0.06326385872187867, + -0.09035816264795715, + -0.14453400360657168, + -0.2516600528059026, + -0.18939684196174453, + -0.12706505707312507, + -0.16329931618554522, + -0.16832508230603466 + ] + }, + { + "line": { + "color": "blue" + }, + "mode": "lines", + "name": "Average", + "type": "scatter", + "x": [ + "0", + "1000000", + "1040000", + "1080000", + "1120000", + "1160000", + "1200000", + "120000", + "1240000", + "1280000", + "1320000", + "1360000", + "1400000", + "1440000", + "1480000", + "1520000", + "1560000", + "160000", + "200000", + "240000", + "280000", + "320000", + "360000", + "400000", + "40000", + "440000", + "480000", + "520000", + "560000", + "600000", + "640000", + "680000", + "720000", + "760000", + "800000", + "80000", + "840000", + "880000", + "920000", + "960000" + ], + "y": [ + 2.3129646346357427e-18, + -2.3129646346357427e-18, + -0.008333333333333337, + 0.008333333333333333, + -0.041666666666666664, + -0.05833333333333334, + 0.041666666666666664, + 0.016666666666666666, + -0.016666666666666666, + -0.016666666666666666, + -0.008333333333333337, + 0.016666666666666666, + 0.06666666666666667, + -0.016666666666666666, + 0, + 0.03333333333333333, + -0.016666666666666666, + 0.033333333333333326, + 0.07500000000000001, + 0.025000000000000005, + -0.025000000000000005, + -0.10833333333333334, + -0.016666666666666666, + -0.008333333333333333, + 0.024999999999999998, + -0.016666666666666666, + -0.00833333333333333, + 0.09166666666666667, + -0.01666666666666667, + -0.025000000000000005, + 0.03333333333333333, + 0.016666666666666666, + -0.016666666666666666, + 0.008333333333333333, + -0.025000000000000005, + 0.041666666666666664, + -0.1166666666666667, + 0.025000000000000005, + 0.05000000000000001, + -0.049999999999999996 + ] + } + ], + "layout": { + "showlegend": true, + "template": { + "data": { + "bar": [ + { + "error_x": { + "color": "#2a3f5f" + }, + "error_y": { + "color": "#2a3f5f" + }, + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "bar" + } + ], + "barpolar": [ + { + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "barpolar" + } + ], + "carpet": [ + { + "aaxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "baxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "type": "carpet" + } + ], + "choropleth": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "choropleth" + } + ], + "contour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "contour" + } + ], + "contourcarpet": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "contourcarpet" + } + ], + "heatmap": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "heatmap" + } + ], + "heatmapgl": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "heatmapgl" + } + ], + "histogram": [ + { + "marker": { + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "histogram" + } + ], + "histogram2d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "histogram2d" + } + ], + "histogram2dcontour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "histogram2dcontour" + } + ], + "mesh3d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "mesh3d" + } + ], + "parcoords": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "parcoords" + } + ], + "pie": [ + { + "automargin": true, + "type": "pie" + } + ], + "scatter": [ + { + "fillpattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + }, + "type": "scatter" + } + ], + "scatter3d": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatter3d" + } + ], + "scattercarpet": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattercarpet" + } + ], + "scattergeo": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergeo" + } + ], + "scattergl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergl" + } + ], + "scattermapbox": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattermapbox" + } + ], + "scatterpolar": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolar" + } + ], + "scatterpolargl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolargl" + } + ], + "scatterternary": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterternary" + } + ], + "surface": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "surface" + } + ], + "table": [ + { + "cells": { + "fill": { + "color": "#EBF0F8" + }, + "line": { + "color": "white" + } + }, + "header": { + "fill": { + "color": "#C8D4E3" + }, + "line": { + "color": "white" + } + }, + "type": "table" + } + ] + }, + "layout": { + "annotationdefaults": { + "arrowcolor": "#2a3f5f", + "arrowhead": 0, + "arrowwidth": 1 + }, + "autotypenumbers": "strict", + "coloraxis": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "colorscale": { + "diverging": [ + [ + 0, + "#8e0152" + ], + [ + 0.1, + "#c51b7d" + ], + [ + 0.2, + "#de77ae" + ], + [ + 0.3, + "#f1b6da" + ], + [ + 0.4, + "#fde0ef" + ], + [ + 0.5, + "#f7f7f7" + ], + [ + 0.6, + "#e6f5d0" + ], + [ + 0.7, + "#b8e186" + ], + [ + 0.8, + "#7fbc41" + ], + [ + 0.9, + "#4d9221" + ], + [ + 1, + "#276419" + ] + ], + "sequential": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "sequentialminus": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ] + }, + "colorway": [ + "#636efa", + "#EF553B", + "#00cc96", + "#ab63fa", + "#FFA15A", + "#19d3f3", + "#FF6692", + "#B6E880", + "#FF97FF", + "#FECB52" + ], + "font": { + "color": "#2a3f5f" + }, + "geo": { + "bgcolor": "white", + "lakecolor": "white", + "landcolor": "#E5ECF6", + "showlakes": true, + "showland": true, + "subunitcolor": "white" + }, + "hoverlabel": { + "align": "left" + }, + "hovermode": "closest", + "mapbox": { + "style": "light" + }, + "paper_bgcolor": "white", + "plot_bgcolor": "#E5ECF6", + "polar": { + "angularaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "radialaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "scene": { + "xaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "yaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "zaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + } + }, + "shapedefaults": { + "line": { + "color": "#2a3f5f" + } + }, + "ternary": { + "aaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "baxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "caxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "title": { + "x": 0.05 + }, + "xaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + }, + "yaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + } + } + }, + "title": { + "text": "DR_PFSP" + }, + "xaxis": { + "title": { + "text": "Checkpoints" + } + }, + "yaxis": { + "title": { + "text": "Returns" + } + } + } + } + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "plot_logs(\"DR_SP\", \"DR_PFSP\")" + ] + }, + { + "cell_type": "code", + "execution_count": 61, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.plotly.v1+json": { + "config": { + "plotlyServerURL": "https://plotly.com" + }, + "data": [ + { + "fill": "toself", + "fillcolor": "rgba(0, 0, 255, 0.2)", + "line": { + "color": "rgba(255, 255, 255, 0)" + }, + "name": "Error Band", + "type": "scatter", + "x": [ + "0", + "1000000", + "1040000", + "1080000", + "1120000", + "1160000", + "1200000", + "120000", + "1240000", + "1280000", + "1320000", + "1360000", + "1400000", + "1440000", + "1480000", + "1520000", + "1560000", + "1600000", + "160000", + "1640000", + "1680000", + "1720000", + "1760000", + "1800000", + "1840000", + "1880000", + "1920000", + "1960000", + "2000000", + "200000", + "2040000", + "2080000", + "2120000", + "2160000", + "2200000", + "2240000", + "2280000", + "2320000", + "2360000", + "2400000", + "240000", + "2440000", + "2480000", + "2520000", + "2560000", + "2600000", + "2640000", + "2680000", + "2720000", + "2760000", + "2800000", + "280000", + "2840000", + "2880000", + "2920000", + "2960000", + "3000000", + "3040000", + "3080000", + "3120000", + "3160000", + "3200000", + "320000", + "3240000", + "3280000", + "3320000", + "3360000", + "3400000", + "3440000", + "3480000", + "3520000", + "3560000", + "3600000", + "360000", + "3640000", + "3680000", + "3720000", + "3760000", + "3800000", + "3840000", + "3880000", + "3920000", + "3960000", + "4000000", + "400000", + "40000", + "4040000", + "4080000", + "4120000", + "4160000", + "4200000", + "4280000", + "4320000", + "4360000", + "440000", + "480000", + "520000", + "560000", + "600000", + "640000", + "680000", + "720000", + "760000", + "800000", + "80000", + "840000", + "880000", + "920000", + "960000", + "960000", + "920000", + "880000", + "840000", + "80000", + "800000", + "760000", + "720000", + "680000", + "640000", + "600000", + "560000", + "520000", + "480000", + "440000", + "4360000", + "4320000", + "4280000", + "4200000", + "4160000", + "4120000", + "4080000", + "4040000", + "40000", + "400000", + "4000000", + "3960000", + "3920000", + "3880000", + "3840000", + "3800000", + "3760000", + "3720000", + "3680000", + "3640000", + "360000", + "3600000", + "3560000", + "3520000", + "3480000", + "3440000", + "3400000", + "3360000", + "3320000", + "3280000", + "3240000", + "320000", + "3200000", + "3160000", + "3120000", + "3080000", + "3040000", + "3000000", + "2960000", + "2920000", + "2880000", + "2840000", + "280000", + "2800000", + "2760000", + "2720000", + "2680000", + "2640000", + "2600000", + "2560000", + "2520000", + "2480000", + "2440000", + "240000", + "2400000", + "2360000", + "2320000", + "2280000", + "2240000", + "2200000", + "2160000", + "2120000", + "2080000", + "2040000", + "200000", + "2000000", + "1960000", + "1920000", + "1880000", + "1840000", + "1800000", + "1760000", + "1720000", + "1680000", + "1640000", + "160000", + "1600000", + "1560000", + "1520000", + "1480000", + "1440000", + "1400000", + "1360000", + "1320000", + "1280000", + "1240000", + "120000", + "1200000", + "1160000", + "1120000", + "1080000", + "1040000", + "1000000", + "0" + ], + "y": [ + 0.09453400360657167, + 0.1046684981546753, + 0.08939684196174451, + 0.1236914959812905, + 0.19035816264795716, + 0.08576347054825484, + 0.1302775637731995, + 0.034253613159535824, + 0.2521780023688185, + 0.08397247358851682, + 0.037046026185496635, + 0.0975942433400174, + 0.08193466305166028, + 0.12552015692526608, + 0.18789364159159508, + 0.21432011823847125, + 0.11072751268321593, + 0.09453400360657166, + 0.15408329997330664, + 0.0760362971081845, + 0.13540064007726602, + 0.07786733693990501, + 0.24394279110594408, + 0.20330065045309284, + 0.15118446353109125, + 0.13092757667335073, + 0.1046684981546753, + 0.19880544678845175, + 0.07786733693990501, + 0.11742113755341183, + 0.2572883909529316, + 0.17706505707312506, + 0.25710249621960596, + 0.08228756555322955, + 0.15805524622579809, + 0.16356613341830967, + 0.13800183148800862, + 0.21174499851199374, + 0.04574271077563382, + 0.14388749301184667, + 0.17706505707312506, + 0.12706505707312507, + 0.16291607312320203, + 0.08704602618549664, + 0.14035816264795717, + 0.17795130420052188, + 0.1152679963849936, + 0.09880544678845173, + 0.16094757082487302, + 0.0760362971081845, + 0.13517389298573831, + 0.10315906800216378, + 0.18263767905168996, + 0.09453400360657166, + 0.02761423749153967, + 0.1836417639099296, + 0.1825741858350554, + 0.15805524622579806, + 0.18240453183331934, + 0.11742113755341183, + 0.0537126928521633, + 0.0979001312335302, + 0.16329931618554522, + 0.0912870929175277, + 0.16039839040645837, + 0.04541582998323942, + 0.23939684196174452, + 0.26263883748662836, + 0.09832207155790618, + 0.12843293866268307, + 0.12395505761959827, + 0.16039839040645837, + 0.13576347054825486, + 0.05205176042696102, + 0.146526032931475, + 0.2527525231651947, + 0.22290199457749038, + 0.13092757667335073, + 0.1152679963849936, + 0.06940690004948294, + 0.09847853524392856, + 0.09541582998323946, + 0.17985936626480836, + 0.24142135623730954, + 0.16794494717703362, + 0.1738996855342767, + -0.009947929376119227, + 0.18388765977766136, + 0.15118446353109127, + 0.04716878364870321, + 0.17369149598129047, + 0.04096733152583014, + 0.1721471334322992, + 0.017421137553411806, + 0.08538509376029435, + 0.1046684981546753, + 0.16722099311099467, + 0.1224744871391589, + 0.14832207155790617, + 0.13517389298573831, + 0.08425361315953582, + 0.14388749301184667, + 0.07071067811865477, + 0.08397247358851682, + 0.077867336939905, + 0.1, + 0.107915619758885, + 0.11224574575382268, + 0.041202265916659646, + -0.1078689325833263, + -0.22891241242048938, + -0.057915619758884984, + -0.1, + -0.09453400360657166, + -0.13397247358851683, + -0.07071067811865477, + -0.07722082634517999, + -0.06758694649286916, + -0.16850722631907164, + -0.09832207155790618, + -0.1224744871391589, + -0.08388765977766136, + -0.13800183148800862, + -0.05205176042696102, + -0.16742113755341181, + -0.23881380009896586, + -0.0576339981924968, + -0.09035816264795715, + -0.09716878364870322, + -0.0845177968644246, + -0.06722099311099472, + -0.22338540395721418, + -0.14056635220094338, + -0.26794494717703365, + -0.04142135623730954, + -0.09652603293147499, + -0.11208249664990612, + -0.2651452019105952, + -0.13607356671614962, + -0.08193466305166028, + -0.0975942433400174, + -0.0729019945774904, + -0.0527525231651947, + -0.12985936626480835, + -0.08538509376029435, + -0.15243013721492152, + -0.07706505707312505, + -0.20728839095293158, + -0.17843293866268306, + -0.14832207155790617, + -0.11263883748662838, + -0.1560635086284112, + -0.1620824966499061, + -0.07706505707312505, + -0.0912870929175277, + -0.16329931618554522, + -0.16456679790019685, + -0.13704602618549663, + -0.06742113755341181, + -0.11573786516665269, + -0.09138857955913138, + -0.1825741858350554, + -0.2669750972432629, + -0.160947570824873, + -0.077867336939905, + -0.21597101238502328, + -0.11982573466883044, + -0.16850722631907164, + -0.1260362971081845, + -0.027614237491539684, + -0.24880544678845173, + -0.08193466305166028, + -0.17795130420052188, + -0.1236914959812905, + -0.10371269285216331, + -0.1295827397898687, + -0.11039839040645841, + -0.06039839040645841, + -0.07722082634517999, + -0.1457427107756338, + -0.17841166517866042, + -0.10466849815467529, + -0.146899466751643, + -0.09138857955913139, + -0.18228756555322953, + -0.023769162886272663, + -0.060398390406458384, + -0.07395505761959825, + -0.06742113755341181, + -0.09453400360657167, + -0.14880544678845176, + -0.13800183148800862, + -0.0975942433400174, + -0.0845177968644246, + -0.1366339837864262, + -0.0939427911059441, + -0.09453400360657167, + -0.13540064007726602, + -0.1260362971081845, + -0.054083299973306624, + -0.077867336939905, + -0.2107275126832159, + -0.19765345157180458, + -0.12122697492492843, + -0.27552015692526605, + -0.1152679963849936, + -0.13092757667335075, + -0.15371269285216332, + -0.13397247358851683, + -0.06884466903548518, + -0.11758694649286916, + -0.23027756377319947, + -0.20243013721492154, + -0.07369149598129052, + -0.14035816264795717, + -0.3060635086284112, + -0.13800183148800862, + -0.07786733693990501 + ] + }, + { + "line": { + "color": "blue" + }, + "mode": "lines", + "name": "Average", + "type": "scatter", + "x": [ + "0", + "1000000", + "1040000", + "1080000", + "1120000", + "1160000", + "1200000", + "120000", + "1240000", + "1280000", + "1320000", + "1360000", + "1400000", + "1440000", + "1480000", + "1520000", + "1560000", + "1600000", + "160000", + "1640000", + "1680000", + "1720000", + "1760000", + "1800000", + "1840000", + "1880000", + "1920000", + "1960000", + "2000000", + "200000", + "2040000", + "2080000", + "2120000", + "2160000", + "2200000", + "2240000", + "2280000", + "2320000", + "2360000", + "2400000", + "240000", + "2440000", + "2480000", + "2520000", + "2560000", + "2600000", + "2640000", + "2680000", + "2720000", + "2760000", + "2800000", + "280000", + "2840000", + "2880000", + "2920000", + "2960000", + "3000000", + "3040000", + "3080000", + "3120000", + "3160000", + "3200000", + "320000", + "3240000", + "3280000", + "3320000", + "3360000", + "3400000", + "3440000", + "3480000", + "3520000", + "3560000", + "3600000", + "360000", + "3640000", + "3680000", + "3720000", + "3760000", + "3800000", + "3840000", + "3880000", + "3920000", + "3960000", + "4000000", + "400000", + "40000", + "4040000", + "4080000", + "4120000", + "4160000", + "4200000", + "4280000", + "4320000", + "4360000", + "440000", + "480000", + "520000", + "560000", + "600000", + "640000", + "680000", + "720000", + "760000", + "800000", + "80000", + "840000", + "880000", + "920000", + "960000" + ], + "y": [ + 0.008333333333333333, + -0.016666666666666666, + -0.10833333333333335, + -0.008333333333333338, + 0.05833333333333333, + -0.05833333333333335, + -0.049999999999999996, + -0.041666666666666664, + 0.09166666666666666, + -0.024999999999999998, + -0.05833333333333334, + -0.01666666666666667, + -0.01666666666666667, + -0.075, + 0.03333333333333333, + 0.008333333333333331, + -0.049999999999999996, + 0.008333333333333333, + 0.05000000000000001, + -0.025000000000000005, + -2.3129646346357427e-18, + -0.008333333333333333, + 0.075, + 0.03333333333333333, + 0.03333333333333333, + 0.016666666666666666, + -0.016666666666666666, + 0.02500000000000001, + -0.008333333333333333, + 0.025000000000000005, + 0.09166666666666667, + 0.05833333333333334, + 0.11666666666666665, + -0.049999999999999996, + 0.03333333333333334, + 0.008333333333333333, + 0.016666666666666666, + 0.01666666666666666, + -0.049999999999999996, + 0.03333333333333333, + 0.05833333333333333, + 0.008333333333333331, + 0.016666666666666666, + -0.008333333333333333, + 0.008333333333333337, + -2.3129646346357427e-18, + 0.016666666666666666, + -0.075, + 0.06666666666666667, + -0.025000000000000005, + -0.016666666666666666, + -0.008333333333333331, + -0.016666666666666666, + 0.008333333333333333, + -0.06666666666666667, + -0.041666666666666664, + 2.3129646346357427e-18, + 0.03333333333333333, + 0.03333333333333333, + 0.025000000000000005, + -0.041666666666666664, + -0.03333333333333333, + -2.3129646346357427e-18, + 0, + 0.041666666666666664, + -0.05833333333333334, + 0.041666666666666664, + 0.075, + -0.024999999999999998, + -0.024999999999999994, + -0.041666666666666664, + 0.041666666666666664, + -0.008333333333333333, + -0.01666666666666667, + 0.008333333333333333, + 0.09999999999999999, + 0.075, + 0.016666666666666666, + 0.016666666666666666, + -0.03333333333333333, + -0.08333333333333333, + -0.008333333333333331, + 0.04166666666666668, + 0.09999999999999999, + -0.05000000000000001, + 0.016666666666666666, + -0.1166666666666667, + 0.05833333333333333, + 0.03333333333333333, + -0.025000000000000005, + 0.041666666666666664, + -0.008333333333333333, + -0.03333333333333334, + -0.07500000000000001, + 0.01666666666666667, + -0.01666666666666667, + 0.041666666666666664, + 0, + 0.024999999999999998, + -0.016666666666666666, + 0.008333333333333333, + 0.03333333333333333, + 0, + -0.025000000000000005, + -0.008333333333333337, + 0, + 0.025000000000000005, + -0.05833333333333335, + -0.03333333333333333 + ] + } + ], + "layout": { + "showlegend": true, + "template": { + "data": { + "bar": [ + { + "error_x": { + "color": "#2a3f5f" + }, + "error_y": { + "color": "#2a3f5f" + }, + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "bar" + } + ], + "barpolar": [ + { + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "barpolar" + } + ], + "carpet": [ + { + "aaxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "baxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "type": "carpet" + } + ], + "choropleth": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "choropleth" + } + ], + "contour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "contour" + } + ], + "contourcarpet": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "contourcarpet" + } + ], + "heatmap": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "heatmap" + } + ], + "heatmapgl": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "heatmapgl" + } + ], + "histogram": [ + { + "marker": { + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "histogram" + } + ], + "histogram2d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "histogram2d" + } + ], + "histogram2dcontour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "histogram2dcontour" + } + ], + "mesh3d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "mesh3d" + } + ], + "parcoords": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "parcoords" + } + ], + "pie": [ + { + "automargin": true, + "type": "pie" + } + ], + "scatter": [ + { + "fillpattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + }, + "type": "scatter" + } + ], + "scatter3d": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatter3d" + } + ], + "scattercarpet": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattercarpet" + } + ], + "scattergeo": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergeo" + } + ], + "scattergl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergl" + } + ], + "scattermapbox": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattermapbox" + } + ], + "scatterpolar": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolar" + } + ], + "scatterpolargl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolargl" + } + ], + "scatterternary": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterternary" + } + ], + "surface": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "surface" + } + ], + "table": [ + { + "cells": { + "fill": { + "color": "#EBF0F8" + }, + "line": { + "color": "white" + } + }, + "header": { + "fill": { + "color": "#C8D4E3" + }, + "line": { + "color": "white" + } + }, + "type": "table" + } + ] + }, + "layout": { + "annotationdefaults": { + "arrowcolor": "#2a3f5f", + "arrowhead": 0, + "arrowwidth": 1 + }, + "autotypenumbers": "strict", + "coloraxis": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "colorscale": { + "diverging": [ + [ + 0, + "#8e0152" + ], + [ + 0.1, + "#c51b7d" + ], + [ + 0.2, + "#de77ae" + ], + [ + 0.3, + "#f1b6da" + ], + [ + 0.4, + "#fde0ef" + ], + [ + 0.5, + "#f7f7f7" + ], + [ + 0.6, + "#e6f5d0" + ], + [ + 0.7, + "#b8e186" + ], + [ + 0.8, + "#7fbc41" + ], + [ + 0.9, + "#4d9221" + ], + [ + 1, + "#276419" + ] + ], + "sequential": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "sequentialminus": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ] + }, + "colorway": [ + "#636efa", + "#EF553B", + "#00cc96", + "#ab63fa", + "#FFA15A", + "#19d3f3", + "#FF6692", + "#B6E880", + "#FF97FF", + "#FECB52" + ], + "font": { + "color": "#2a3f5f" + }, + "geo": { + "bgcolor": "white", + "lakecolor": "white", + "landcolor": "#E5ECF6", + "showlakes": true, + "showland": true, + "subunitcolor": "white" + }, + "hoverlabel": { + "align": "left" + }, + "hovermode": "closest", + "mapbox": { + "style": "light" + }, + "paper_bgcolor": "white", + "plot_bgcolor": "#E5ECF6", + "polar": { + "angularaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "radialaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "scene": { + "xaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "yaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "zaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + } + }, + "shapedefaults": { + "line": { + "color": "#2a3f5f" + } + }, + "ternary": { + "aaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "baxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "caxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "title": { + "x": 0.05 + }, + "xaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + }, + "yaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + } + } + }, + "title": { + "text": "DR_FSP" + }, + "xaxis": { + "title": { + "text": "Checkpoints" + } + }, + "yaxis": { + "title": { + "text": "Returns" + } + } + } + } + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.plotly.v1+json": { + "config": { + "plotlyServerURL": "https://plotly.com" + }, + "data": [ + { + "fill": "toself", + "fillcolor": "rgba(0, 0, 255, 0.2)", + "line": { + "color": "rgba(255, 255, 255, 0)" + }, + "name": "Error Band", + "type": "scatter", + "x": [ + "0", + "1000000", + "1040000", + "1080000", + "1120000", + "1160000", + "1200000", + "120000", + "1240000", + "1280000", + "1320000", + "1360000", + "1400000", + "1440000", + "1480000", + "1520000", + "1560000", + "1600000", + "160000", + "1640000", + "1680000", + "1720000", + "1760000", + "1800000", + "1840000", + "1880000", + "1920000", + "1960000", + "2000000", + "200000", + "2040000", + "2080000", + "2120000", + "2160000", + "2200000", + "2240000", + "2280000", + "2320000", + "2360000", + "2400000", + "240000", + "2440000", + "2480000", + "2520000", + "2560000", + "2600000", + "2640000", + "2680000", + "2720000", + "2760000", + "2800000", + "280000", + "2840000", + "2880000", + "2920000", + "2960000", + "3000000", + "3040000", + "3080000", + "3120000", + "3160000", + "3200000", + "320000", + "3240000", + "3280000", + "3320000", + "3360000", + "3400000", + "3440000", + "3480000", + "3520000", + "3560000", + "3600000", + "360000", + "3640000", + "3680000", + "3720000", + "3760000", + "3800000", + "3840000", + "3880000", + "3920000", + "3960000", + "4000000", + "400000", + "40000", + "4040000", + "4080000", + "4120000", + "4160000", + "4200000", + "4280000", + "4320000", + "4360000", + "440000", + "480000", + "520000", + "560000", + "600000", + "640000", + "680000", + "720000", + "760000", + "800000", + "80000", + "840000", + "880000", + "920000", + "960000", + "960000", + "920000", + "880000", + "840000", + "80000", + "800000", + "760000", + "720000", + "680000", + "640000", + "600000", + "560000", + "520000", + "480000", + "440000", + "4360000", + "4320000", + "4280000", + "4200000", + "4160000", + "4120000", + "4080000", + "4040000", + "40000", + "400000", + "4000000", + "3960000", + "3920000", + "3880000", + "3840000", + "3800000", + "3760000", + "3720000", + "3680000", + "3640000", + "360000", + "3600000", + "3560000", + "3520000", + "3480000", + "3440000", + "3400000", + "3360000", + "3320000", + "3280000", + "3240000", + "320000", + "3200000", + "3160000", + "3120000", + "3080000", + "3040000", + "3000000", + "2960000", + "2920000", + "2880000", + "2840000", + "280000", + "2800000", + "2760000", + "2720000", + "2680000", + "2640000", + "2600000", + "2560000", + "2520000", + "2480000", + "2440000", + "240000", + "2400000", + "2360000", + "2320000", + "2280000", + "2240000", + "2200000", + "2160000", + "2120000", + "2080000", + "2040000", + "200000", + "2000000", + "1960000", + "1920000", + "1880000", + "1840000", + "1800000", + "1760000", + "1720000", + "1680000", + "1640000", + "160000", + "1600000", + "1560000", + "1520000", + "1480000", + "1440000", + "1400000", + "1360000", + "1320000", + "1280000", + "1240000", + "120000", + "1200000", + "1160000", + "1120000", + "1080000", + "1040000", + "1000000", + "0" + ], + "y": [ + 0.07786733693990501, + 0.13800183148800862, + 0.3060635086284112, + 0.14035816264795717, + 0.07369149598129052, + 0.20243013721492154, + 0.23027756377319947, + 0.11758694649286916, + 0.06884466903548518, + 0.13397247358851683, + 0.15371269285216332, + 0.13092757667335075, + 0.1152679963849936, + 0.27552015692526605, + 0.12122697492492843, + 0.19765345157180458, + 0.2107275126832159, + 0.077867336939905, + 0.054083299973306624, + 0.1260362971081845, + 0.13540064007726602, + 0.09453400360657167, + 0.0939427911059441, + 0.1366339837864262, + 0.0845177968644246, + 0.0975942433400174, + 0.13800183148800862, + 0.14880544678845176, + 0.09453400360657167, + 0.06742113755341181, + 0.07395505761959825, + 0.060398390406458384, + 0.023769162886272663, + 0.18228756555322953, + 0.09138857955913139, + 0.146899466751643, + 0.10466849815467529, + 0.17841166517866042, + 0.1457427107756338, + 0.07722082634517999, + 0.06039839040645841, + 0.11039839040645841, + 0.1295827397898687, + 0.10371269285216331, + 0.1236914959812905, + 0.17795130420052188, + 0.08193466305166028, + 0.24880544678845173, + 0.027614237491539684, + 0.1260362971081845, + 0.16850722631907164, + 0.11982573466883044, + 0.21597101238502328, + 0.077867336939905, + 0.160947570824873, + 0.2669750972432629, + 0.1825741858350554, + 0.09138857955913138, + 0.11573786516665269, + 0.06742113755341181, + 0.13704602618549663, + 0.16456679790019685, + 0.16329931618554522, + 0.0912870929175277, + 0.07706505707312505, + 0.1620824966499061, + 0.1560635086284112, + 0.11263883748662838, + 0.14832207155790617, + 0.17843293866268306, + 0.20728839095293158, + 0.07706505707312505, + 0.15243013721492152, + 0.08538509376029435, + 0.12985936626480835, + 0.0527525231651947, + 0.0729019945774904, + 0.0975942433400174, + 0.08193466305166028, + 0.13607356671614962, + 0.2651452019105952, + 0.11208249664990612, + 0.09652603293147499, + 0.04142135623730954, + 0.26794494717703365, + 0.14056635220094338, + 0.22338540395721418, + 0.06722099311099472, + 0.0845177968644246, + 0.09716878364870322, + 0.09035816264795715, + 0.0576339981924968, + 0.23881380009896586, + 0.16742113755341181, + 0.05205176042696102, + 0.13800183148800862, + 0.08388765977766136, + 0.1224744871391589, + 0.09832207155790618, + 0.16850722631907164, + 0.06758694649286916, + 0.07722082634517999, + 0.07071067811865477, + 0.13397247358851683, + 0.09453400360657166, + 0.1, + 0.057915619758884984, + 0.22891241242048938, + 0.1078689325833263, + -0.041202265916659646, + -0.11224574575382268, + -0.107915619758885, + -0.1, + -0.077867336939905, + -0.08397247358851682, + -0.07071067811865477, + -0.14388749301184667, + -0.08425361315953582, + -0.13517389298573831, + -0.14832207155790617, + -0.1224744871391589, + -0.16722099311099467, + -0.1046684981546753, + -0.08538509376029435, + -0.017421137553411806, + -0.1721471334322992, + -0.04096733152583014, + -0.17369149598129047, + -0.04716878364870321, + -0.15118446353109127, + -0.18388765977766136, + 0.009947929376119227, + -0.1738996855342767, + -0.16794494717703362, + -0.24142135623730954, + -0.17985936626480836, + -0.09541582998323946, + -0.09847853524392856, + -0.06940690004948294, + -0.1152679963849936, + -0.13092757667335073, + -0.22290199457749038, + -0.2527525231651947, + -0.146526032931475, + -0.05205176042696102, + -0.13576347054825486, + -0.16039839040645837, + -0.12395505761959827, + -0.12843293866268307, + -0.09832207155790618, + -0.26263883748662836, + -0.23939684196174452, + -0.04541582998323942, + -0.16039839040645837, + -0.0912870929175277, + -0.16329931618554522, + -0.0979001312335302, + -0.0537126928521633, + -0.11742113755341183, + -0.18240453183331934, + -0.15805524622579806, + -0.1825741858350554, + -0.1836417639099296, + -0.02761423749153967, + -0.09453400360657166, + -0.18263767905168996, + -0.10315906800216378, + -0.13517389298573831, + -0.0760362971081845, + -0.16094757082487302, + -0.09880544678845173, + -0.1152679963849936, + -0.17795130420052188, + -0.14035816264795717, + -0.08704602618549664, + -0.16291607312320203, + -0.12706505707312507, + -0.17706505707312506, + -0.14388749301184667, + -0.04574271077563382, + -0.21174499851199374, + -0.13800183148800862, + -0.16356613341830967, + -0.15805524622579809, + -0.08228756555322955, + -0.25710249621960596, + -0.17706505707312506, + -0.2572883909529316, + -0.11742113755341183, + -0.07786733693990501, + -0.19880544678845175, + -0.1046684981546753, + -0.13092757667335073, + -0.15118446353109125, + -0.20330065045309284, + -0.24394279110594408, + -0.07786733693990501, + -0.13540064007726602, + -0.0760362971081845, + -0.15408329997330664, + -0.09453400360657166, + -0.11072751268321593, + -0.21432011823847125, + -0.18789364159159508, + -0.12552015692526608, + -0.08193466305166028, + -0.0975942433400174, + -0.037046026185496635, + -0.08397247358851682, + -0.2521780023688185, + -0.034253613159535824, + -0.1302775637731995, + -0.08576347054825484, + -0.19035816264795716, + -0.1236914959812905, + -0.08939684196174451, + -0.1046684981546753, + -0.09453400360657167 + ] + }, + { + "line": { + "color": "blue" + }, + "mode": "lines", + "name": "Average", + "type": "scatter", + "x": [ + "0", + "1000000", + "1040000", + "1080000", + "1120000", + "1160000", + "1200000", + "120000", + "1240000", + "1280000", + "1320000", + "1360000", + "1400000", + "1440000", + "1480000", + "1520000", + "1560000", + "1600000", + "160000", + "1640000", + "1680000", + "1720000", + "1760000", + "1800000", + "1840000", + "1880000", + "1920000", + "1960000", + "2000000", + "200000", + "2040000", + "2080000", + "2120000", + "2160000", + "2200000", + "2240000", + "2280000", + "2320000", + "2360000", + "2400000", + "240000", + "2440000", + "2480000", + "2520000", + "2560000", + "2600000", + "2640000", + "2680000", + "2720000", + "2760000", + "2800000", + "280000", + "2840000", + "2880000", + "2920000", + "2960000", + "3000000", + "3040000", + "3080000", + "3120000", + "3160000", + "3200000", + "320000", + "3240000", + "3280000", + "3320000", + "3360000", + "3400000", + "3440000", + "3480000", + "3520000", + "3560000", + "3600000", + "360000", + "3640000", + "3680000", + "3720000", + "3760000", + "3800000", + "3840000", + "3880000", + "3920000", + "3960000", + "4000000", + "400000", + "40000", + "4040000", + "4080000", + "4120000", + "4160000", + "4200000", + "4280000", + "4320000", + "4360000", + "440000", + "480000", + "520000", + "560000", + "600000", + "640000", + "680000", + "720000", + "760000", + "800000", + "80000", + "840000", + "880000", + "920000", + "960000" + ], + "y": [ + -0.008333333333333333, + 0.016666666666666666, + 0.10833333333333335, + 0.008333333333333338, + -0.05833333333333333, + 0.05833333333333335, + 0.049999999999999996, + 0.041666666666666664, + -0.09166666666666666, + 0.024999999999999998, + 0.05833333333333334, + 0.01666666666666667, + 0.01666666666666667, + 0.075, + -0.03333333333333333, + -0.008333333333333331, + 0.049999999999999996, + -0.008333333333333333, + -0.05000000000000001, + 0.025000000000000005, + 2.3129646346357427e-18, + 0.008333333333333333, + -0.075, + -0.03333333333333333, + -0.03333333333333333, + -0.016666666666666666, + 0.016666666666666666, + -0.02500000000000001, + 0.008333333333333333, + -0.025000000000000005, + -0.09166666666666667, + -0.05833333333333334, + -0.11666666666666665, + 0.049999999999999996, + -0.03333333333333334, + -0.008333333333333333, + -0.016666666666666666, + -0.01666666666666666, + 0.049999999999999996, + -0.03333333333333333, + -0.05833333333333333, + -0.008333333333333331, + -0.016666666666666666, + 0.008333333333333333, + -0.008333333333333337, + 2.3129646346357427e-18, + -0.016666666666666666, + 0.075, + -0.06666666666666667, + 0.025000000000000005, + 0.016666666666666666, + 0.008333333333333331, + 0.016666666666666666, + -0.008333333333333333, + 0.06666666666666667, + 0.041666666666666664, + -2.3129646346357427e-18, + -0.03333333333333333, + -0.03333333333333333, + -0.025000000000000005, + 0.041666666666666664, + 0.03333333333333333, + 2.3129646346357427e-18, + 0, + -0.041666666666666664, + 0.05833333333333334, + -0.041666666666666664, + -0.075, + 0.024999999999999998, + 0.024999999999999994, + 0.041666666666666664, + -0.041666666666666664, + 0.008333333333333333, + 0.01666666666666667, + -0.008333333333333333, + -0.09999999999999999, + -0.075, + -0.016666666666666666, + -0.016666666666666666, + 0.03333333333333333, + 0.08333333333333333, + 0.008333333333333331, + -0.04166666666666668, + -0.09999999999999999, + 0.05000000000000001, + -0.016666666666666666, + 0.1166666666666667, + -0.05833333333333333, + -0.03333333333333333, + 0.025000000000000005, + -0.041666666666666664, + 0.008333333333333333, + 0.03333333333333334, + 0.07500000000000001, + -0.01666666666666667, + 0.01666666666666667, + -0.041666666666666664, + 0, + -0.024999999999999998, + 0.016666666666666666, + -0.008333333333333333, + -0.03333333333333333, + 0, + 0.025000000000000005, + 0.008333333333333337, + 0, + -0.025000000000000005, + 0.05833333333333335, + 0.03333333333333333 + ] + } + ], + "layout": { + "showlegend": true, + "template": { + "data": { + "bar": [ + { + "error_x": { + "color": "#2a3f5f" + }, + "error_y": { + "color": "#2a3f5f" + }, + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "bar" + } + ], + "barpolar": [ + { + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "barpolar" + } + ], + "carpet": [ + { + "aaxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "baxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "type": "carpet" + } + ], + "choropleth": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "choropleth" + } + ], + "contour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "contour" + } + ], + "contourcarpet": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "contourcarpet" + } + ], + "heatmap": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "heatmap" + } + ], + "heatmapgl": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "heatmapgl" + } + ], + "histogram": [ + { + "marker": { + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "histogram" + } + ], + "histogram2d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "histogram2d" + } + ], + "histogram2dcontour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "histogram2dcontour" + } + ], + "mesh3d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "mesh3d" + } + ], + "parcoords": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "parcoords" + } + ], + "pie": [ + { + "automargin": true, + "type": "pie" + } + ], + "scatter": [ + { + "fillpattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + }, + "type": "scatter" + } + ], + "scatter3d": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatter3d" + } + ], + "scattercarpet": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattercarpet" + } + ], + "scattergeo": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergeo" + } + ], + "scattergl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergl" + } + ], + "scattermapbox": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattermapbox" + } + ], + "scatterpolar": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolar" + } + ], + "scatterpolargl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolargl" + } + ], + "scatterternary": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterternary" + } + ], + "surface": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "surface" + } + ], + "table": [ + { + "cells": { + "fill": { + "color": "#EBF0F8" + }, + "line": { + "color": "white" + } + }, + "header": { + "fill": { + "color": "#C8D4E3" + }, + "line": { + "color": "white" + } + }, + "type": "table" + } + ] + }, + "layout": { + "annotationdefaults": { + "arrowcolor": "#2a3f5f", + "arrowhead": 0, + "arrowwidth": 1 + }, + "autotypenumbers": "strict", + "coloraxis": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "colorscale": { + "diverging": [ + [ + 0, + "#8e0152" + ], + [ + 0.1, + "#c51b7d" + ], + [ + 0.2, + "#de77ae" + ], + [ + 0.3, + "#f1b6da" + ], + [ + 0.4, + "#fde0ef" + ], + [ + 0.5, + "#f7f7f7" + ], + [ + 0.6, + "#e6f5d0" + ], + [ + 0.7, + "#b8e186" + ], + [ + 0.8, + "#7fbc41" + ], + [ + 0.9, + "#4d9221" + ], + [ + 1, + "#276419" + ] + ], + "sequential": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "sequentialminus": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ] + }, + "colorway": [ + "#636efa", + "#EF553B", + "#00cc96", + "#ab63fa", + "#FFA15A", + "#19d3f3", + "#FF6692", + "#B6E880", + "#FF97FF", + "#FECB52" + ], + "font": { + "color": "#2a3f5f" + }, + "geo": { + "bgcolor": "white", + "lakecolor": "white", + "landcolor": "#E5ECF6", + "showlakes": true, + "showland": true, + "subunitcolor": "white" + }, + "hoverlabel": { + "align": "left" + }, + "hovermode": "closest", + "mapbox": { + "style": "light" + }, + "paper_bgcolor": "white", + "plot_bgcolor": "#E5ECF6", + "polar": { + "angularaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "radialaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "scene": { + "xaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "yaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "zaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + } + }, + "shapedefaults": { + "line": { + "color": "#2a3f5f" + } + }, + "ternary": { + "aaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "baxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "caxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "title": { + "x": 0.05 + }, + "xaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + }, + "yaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + } + } + }, + "title": { + "text": "DR_PFSP" + }, + "xaxis": { + "title": { + "text": "Checkpoints" + } + }, + "yaxis": { + "title": { + "text": "Returns" + } + } + } + } + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "plot_logs(\"DR_FSP\", \"DR_PFSP\")" + ] + }, + { + "cell_type": "code", + "execution_count": 62, + "metadata": {}, + "outputs": [], + "source": [ + "def plot_logs(agent_1: str, agent_2: str):\n", + " with open(f\"parse_results/{agent_1}_{agent_2}_returns.json\", \"r\") as f:\n", + " logs = json.load(f)\n", + " plot_returns(pd.Series(logs[agent_1]), agent_1)\n", + " plot_returns(pd.Series(logs[agent_2]), agent_2)" + ] + }, + { + "cell_type": "code", + "execution_count": 63, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "DR_FSP_DR_PFSP_returns.json\n", + "DR_SP_DR_FSP_returns.json\n", + "DR_SP_DR_PFSP_returns.json\n" + ] + } + ], + "source": [ + "df = {}\n", + "logs = [file for file in os.listdir(\"parse_results\") if \".json\" in file]\n", + "for log in logs:\n", + " print(log) \n", + " with open(f\"parse_results/{log}\", \"r\") as f:\n", + " returns = json.load(f)\n", + "\n", + " df[log] = returns" + ] + }, + { + "cell_type": "code", + "execution_count": 64, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "DR_FSP\n", + "109\n", + "DR_PFSP\n", + "109\n", + "DR_SP\n", + "40\n", + "DR_FSP\n", + "40\n", + "DR_SP\n", + "40\n", + "DR_PFSP\n", + "40\n" + ] + } + ], + "source": [ + "for log in list(df.keys()):\n", + " for agent in df[log].keys():\n", + " print(agent)\n", + " print(len(list(df[log][agent].keys())))" + ] + }, + { + "cell_type": "code", + "execution_count": 65, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "DR_FSP_DR_PFSP_returns.json\n", + "DR_SP_DR_FSP_returns.json\n", + "DR_SP_DR_PFSP_returns.json\n" + ] + }, + { + "data": { + "application/vnd.plotly.v1+json": { + "config": { + "plotlyServerURL": "https://plotly.com" + }, + "data": [ + { + "mode": "lines", + "name": "DR_FSP", + "type": "scatter", + "x": [ + 0, + 40000, + 80000, + 120000, + 160000, + 200000, + 240000, + 280000, + 320000, + 360000, + 400000, + 440000, + 480000, + 520000, + 560000, + 600000, + 640000, + 680000, + 720000, + 760000, + 800000, + 840000, + 880000, + 920000, + 960000, + 1000000, + 1040000, + 1080000, + 1120000, + 1160000, + 1200000, + 1240000, + 1280000, + 1320000, + 1360000, + 1400000, + 1440000, + 1480000, + 1520000, + 1560000, + 1600000, + 1640000, + 1680000, + 1720000, + 1760000, + 1800000, + 1840000, + 1880000, + 1920000, + 1960000, + 2000000, + 2040000, + 2080000, + 2120000, + 2160000, + 2200000, + 2240000, + 2280000, + 2320000, + 2360000, + 2400000, + 2440000, + 2480000, + 2520000, + 2560000, + 2600000, + 2640000, + 2680000, + 2720000, + 2760000, + 2800000, + 2840000, + 2880000, + 2920000, + 2960000, + 3000000, + 3040000, + 3080000, + 3120000, + 3160000, + 3200000, + 3240000, + 3280000, + 3320000, + 3360000, + 3400000, + 3440000, + 3480000, + 3520000, + 3560000, + 3600000, + 3640000, + 3680000, + 3720000, + 3760000, + 3800000, + 3840000, + 3880000, + 3920000, + 3960000, + 4000000, + 4040000, + 4080000, + 4120000, + 4160000, + 4200000, + 4280000, + 4320000, + 4360000 + ], + "y": [ + -0.016666666666666666, + 0.00833333333333333, + -0.016666666666666666, + -0.01666666666666667, + 0.025000000000000005, + 0, + 0.037500000000000006, + -0.012499999999999999, + -0.008333333333333333, + 0, + 0, + 0.025000000000000005, + 0.012499999999999997, + 0.016666666666666663, + 0.008333333333333337, + 0.024999999999999998, + 0.004166666666666668, + 0.025000000000000005, + 0.016666666666666666, + -0.004166666666666667, + 0.004166666666666668, + 0.004166666666666666, + 2.3129646346357427e-18, + -0.004166666666666667, + -0.016666666666666666, + 0.020833333333333332, + -0.024999999999999998, + 0.016666666666666666, + 0.04583333333333334, + -0.025000000000000005, + -0.012500000000000004, + 0.049999999999999996, + -0.008333333333333333, + -0.00416666666666667, + 0.004166666666666666, + -0.020833333333333332, + -0.02916666666666667, + 0.008333333333333333, + -0.029166666666666664, + -0.016666666666666666, + 0.008333333333333333, + -0.025000000000000005, + -2.3129646346357427e-18, + -0.008333333333333333, + 0.075, + 0.03333333333333333, + 0.03333333333333333, + 0.016666666666666666, + -0.016666666666666666, + 0.02500000000000001, + -0.008333333333333333, + 0.09166666666666667, + 0.05833333333333334, + 0.11666666666666665, + -0.049999999999999996, + 0.03333333333333334, + 0.008333333333333333, + 0.016666666666666666, + 0.01666666666666666, + -0.049999999999999996, + 0.03333333333333333, + 0.008333333333333331, + 0.016666666666666666, + -0.008333333333333333, + 0.008333333333333337, + -2.3129646346357427e-18, + 0.016666666666666666, + -0.075, + 0.06666666666666667, + -0.025000000000000005, + -0.016666666666666666, + -0.016666666666666666, + 0.008333333333333333, + -0.06666666666666667, + -0.041666666666666664, + 2.3129646346357427e-18, + 0.03333333333333333, + 0.03333333333333333, + 0.025000000000000005, + -0.041666666666666664, + -0.03333333333333333, + 0, + 0.041666666666666664, + -0.05833333333333334, + 0.041666666666666664, + 0.075, + -0.024999999999999998, + -0.024999999999999994, + -0.041666666666666664, + 0.041666666666666664, + -0.008333333333333333, + 0.008333333333333333, + 0.09999999999999999, + 0.075, + 0.016666666666666666, + 0.016666666666666666, + -0.03333333333333333, + -0.08333333333333333, + -0.008333333333333331, + 0.04166666666666668, + 0.09999999999999999, + -0.1166666666666667, + 0.05833333333333333, + 0.03333333333333333, + -0.025000000000000005, + 0.041666666666666664, + -0.008333333333333333, + -0.03333333333333334, + -0.07500000000000001 + ] + }, + { + "mode": "lines", + "name": "DR_PFSP", + "type": "scatter", + "x": [ + 0, + 40000, + 80000, + 120000, + 160000, + 200000, + 240000, + 280000, + 320000, + 360000, + 400000, + 440000, + 480000, + 520000, + 560000, + 600000, + 640000, + 680000, + 720000, + 760000, + 800000, + 840000, + 880000, + 920000, + 960000, + 1000000, + 1040000, + 1080000, + 1120000, + 1160000, + 1200000, + 1240000, + 1280000, + 1320000, + 1360000, + 1400000, + 1440000, + 1480000, + 1520000, + 1560000, + 1600000, + 1640000, + 1680000, + 1720000, + 1760000, + 1800000, + 1840000, + 1880000, + 1920000, + 1960000, + 2000000, + 2040000, + 2080000, + 2120000, + 2160000, + 2200000, + 2240000, + 2280000, + 2320000, + 2360000, + 2400000, + 2440000, + 2480000, + 2520000, + 2560000, + 2600000, + 2640000, + 2680000, + 2720000, + 2760000, + 2800000, + 2840000, + 2880000, + 2920000, + 2960000, + 3000000, + 3040000, + 3080000, + 3120000, + 3160000, + 3200000, + 3240000, + 3280000, + 3320000, + 3360000, + 3400000, + 3440000, + 3480000, + 3520000, + 3560000, + 3600000, + 3640000, + 3680000, + 3720000, + 3760000, + 3800000, + 3840000, + 3880000, + 3920000, + 3960000, + 4000000, + 4040000, + 4080000, + 4120000, + 4160000, + 4200000, + 4280000, + 4320000, + 4360000 + ], + "y": [ + -0.004166666666666666, + 0.004166666666666668, + 0.025000000000000005, + 0.02916666666666667, + -0.008333333333333331, + 0.025000000000000005, + -0.016666666666666666, + -0.008333333333333337, + -0.054166666666666675, + 0, + 0.020833333333333332, + -0.016666666666666666, + 0.004166666666666668, + 0.024999999999999998, + -0.008333333333333333, + -0.025000000000000005, + 0.025000000000000005, + 0.004166666666666666, + -0.024999999999999998, + 0.004166666666666667, + 0, + -0.05833333333333334, + 0, + 0.05416666666666667, + -0.008333333333333331, + 0.008333333333333331, + 0.05000000000000001, + 0.008333333333333337, + -0.049999999999999996, + 4.625929269271485e-18, + 0.04583333333333334, + -0.05416666666666667, + 0.004166666666666667, + 0.024999999999999998, + 0.016666666666666666, + 0.041666666666666664, + 0.029166666666666664, + -0.016666666666666666, + 0.012499999999999997, + 0.016666666666666666, + -0.008333333333333333, + 0.025000000000000005, + 2.3129646346357427e-18, + 0.008333333333333333, + -0.075, + -0.03333333333333333, + -0.03333333333333333, + -0.016666666666666666, + 0.016666666666666666, + -0.02500000000000001, + 0.008333333333333333, + -0.09166666666666667, + -0.05833333333333334, + -0.11666666666666665, + 0.049999999999999996, + -0.03333333333333334, + -0.008333333333333333, + -0.016666666666666666, + -0.01666666666666666, + 0.049999999999999996, + -0.03333333333333333, + -0.008333333333333331, + -0.016666666666666666, + 0.008333333333333333, + -0.008333333333333337, + 2.3129646346357427e-18, + -0.016666666666666666, + 0.075, + -0.06666666666666667, + 0.025000000000000005, + 0.016666666666666666, + 0.016666666666666666, + -0.008333333333333333, + 0.06666666666666667, + 0.041666666666666664, + -2.3129646346357427e-18, + -0.03333333333333333, + -0.03333333333333333, + -0.025000000000000005, + 0.041666666666666664, + 0.03333333333333333, + 0, + -0.041666666666666664, + 0.05833333333333334, + -0.041666666666666664, + -0.075, + 0.024999999999999998, + 0.024999999999999994, + 0.041666666666666664, + -0.041666666666666664, + 0.008333333333333333, + -0.008333333333333333, + -0.09999999999999999, + -0.075, + -0.016666666666666666, + -0.016666666666666666, + 0.03333333333333333, + 0.08333333333333333, + 0.008333333333333331, + -0.04166666666666668, + -0.09999999999999999, + 0.1166666666666667, + -0.05833333333333333, + -0.03333333333333333, + 0.025000000000000005, + -0.041666666666666664, + 0.008333333333333333, + 0.03333333333333334, + 0.07500000000000001 + ] + }, + { + "mode": "lines", + "name": "DR_SP", + "type": "scatter", + "x": [ + 0, + 40000, + 80000, + 120000, + 160000, + 200000, + 240000, + 280000, + 320000, + 360000, + 400000, + 440000, + 480000, + 520000, + 560000, + 600000, + 640000, + 680000, + 720000, + 760000, + 800000, + 840000, + 880000, + 920000, + 960000, + 1000000, + 1040000, + 1080000, + 1120000, + 1160000, + 1200000, + 1240000, + 1280000, + 1320000, + 1360000, + 1400000, + 1440000, + 1480000, + 1520000, + 1560000, + 1600000, + 1640000, + 1680000, + 1720000, + 1760000, + 1800000, + 1840000, + 1880000, + 1920000, + 1960000, + 2000000, + 2040000, + 2080000, + 2120000, + 2160000, + 2200000, + 2240000, + 2280000, + 2320000, + 2360000, + 2400000, + 2440000, + 2480000, + 2520000, + 2560000, + 2600000, + 2640000, + 2680000, + 2720000, + 2760000, + 2800000, + 2840000, + 2880000, + 2920000, + 2960000, + 3000000, + 3040000, + 3080000, + 3120000, + 3160000, + 3200000, + 3240000, + 3280000, + 3320000, + 3360000, + 3400000, + 3440000, + 3480000, + 3520000, + 3560000, + 3600000, + 3640000, + 3680000, + 3720000, + 3760000, + 3800000, + 3840000, + 3880000, + 3920000, + 3960000, + 4000000, + 4040000, + 4080000, + 4120000, + 4160000, + 4200000, + 4280000, + 4320000, + 4360000 + ], + "y": [ + 0.027777777777777783, + -0.008333333333333331, + 0.002777777777777775, + -0.011111111111111112, + -0.011111111111111112, + -0.008333333333333335, + -0.019444444444444445, + 0.019444444444444445, + 0.04722222222222223, + -0.0055555555555555575, + -0.030555555555555558, + -0.01666666666666667, + -0.025000000000000005, + -0.024999999999999994, + -0.005555555555555557, + -0.008333333333333335, + -0.027777777777777776, + -0.03333333333333333, + 0.005555555555555554, + 0.002777777777777777, + -0.013888888888888892, + 0.03333333333333334, + 0.008333333333333331, + -0.049999999999999996, + 0.016666666666666666, + -0.03888888888888889, + -0.03611111111111112, + -0.03055555555555555, + -0.008333333333333342, + 0.013888888888888892, + -0.030555555555555544, + 0, + -1.5419764230904951e-18, + -0.03055555555555555, + -0.022222222222222227, + -0.005555555555555553, + -0.005555555555555556, + 0.011111111111111115, + 0.03333333333333334, + -0.005555555555555557, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ] + } + ], + "layout": { + "showlegend": true, + "template": { + "data": { + "bar": [ + { + "error_x": { + "color": "#2a3f5f" + }, + "error_y": { + "color": "#2a3f5f" + }, + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "bar" + } + ], + "barpolar": [ + { + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "barpolar" + } + ], + "carpet": [ + { + "aaxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "baxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "type": "carpet" + } + ], + "choropleth": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "choropleth" + } + ], + "contour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "contour" + } + ], + "contourcarpet": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "contourcarpet" + } + ], + "heatmap": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "heatmap" + } + ], + "heatmapgl": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "heatmapgl" + } + ], + "histogram": [ + { + "marker": { + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "histogram" + } + ], + "histogram2d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "histogram2d" + } + ], + "histogram2dcontour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "histogram2dcontour" + } + ], + "mesh3d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "mesh3d" + } + ], + "parcoords": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "parcoords" + } + ], + "pie": [ + { + "automargin": true, + "type": "pie" + } + ], + "scatter": [ + { + "fillpattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + }, + "type": "scatter" + } + ], + "scatter3d": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatter3d" + } + ], + "scattercarpet": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattercarpet" + } + ], + "scattergeo": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergeo" + } + ], + "scattergl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergl" + } + ], + "scattermapbox": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattermapbox" + } + ], + "scatterpolar": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolar" + } + ], + "scatterpolargl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolargl" + } + ], + "scatterternary": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterternary" + } + ], + "surface": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "surface" + } + ], + "table": [ + { + "cells": { + "fill": { + "color": "#EBF0F8" + }, + "line": { + "color": "white" + } + }, + "header": { + "fill": { + "color": "#C8D4E3" + }, + "line": { + "color": "white" + } + }, + "type": "table" + } + ] + }, + "layout": { + "annotationdefaults": { + "arrowcolor": "#2a3f5f", + "arrowhead": 0, + "arrowwidth": 1 + }, + "autotypenumbers": "strict", + "coloraxis": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "colorscale": { + "diverging": [ + [ + 0, + "#8e0152" + ], + [ + 0.1, + "#c51b7d" + ], + [ + 0.2, + "#de77ae" + ], + [ + 0.3, + "#f1b6da" + ], + [ + 0.4, + "#fde0ef" + ], + [ + 0.5, + "#f7f7f7" + ], + [ + 0.6, + "#e6f5d0" + ], + [ + 0.7, + "#b8e186" + ], + [ + 0.8, + "#7fbc41" + ], + [ + 0.9, + "#4d9221" + ], + [ + 1, + "#276419" + ] + ], + "sequential": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "sequentialminus": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ] + }, + "colorway": [ + "#636efa", + "#EF553B", + "#00cc96", + "#ab63fa", + "#FFA15A", + "#19d3f3", + "#FF6692", + "#B6E880", + "#FF97FF", + "#FECB52" + ], + "font": { + "color": "#2a3f5f" + }, + "geo": { + "bgcolor": "white", + "lakecolor": "white", + "landcolor": "#E5ECF6", + "showlakes": true, + "showland": true, + "subunitcolor": "white" + }, + "hoverlabel": { + "align": "left" + }, + "hovermode": "closest", + "mapbox": { + "style": "light" + }, + "paper_bgcolor": "white", + "plot_bgcolor": "#E5ECF6", + "polar": { + "angularaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "radialaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "scene": { + "xaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "yaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "zaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + } + }, + "shapedefaults": { + "line": { + "color": "#2a3f5f" + } + }, + "ternary": { + "aaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "baxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "caxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "title": { + "x": 0.05 + }, + "xaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + }, + "yaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + } + } + }, + "title": { + "text": "RR returns" + }, + "xaxis": { + "title": { + "text": "Checkpoints" + } + }, + "yaxis": { + "title": { + "text": "Returns" + } + } + } + } + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "df = {}\n", + "logs = [file for file in os.listdir(\"parse_results\") if \".json\" in file]\n", + "for log in logs:\n", + " print(log) \n", + " with open(f\"parse_results/{log}\", \"r\") as f:\n", + " returns = json.load(f)\n", + "\n", + " agent_1, agent_2 = list(returns.keys())\n", + " if agent_1 not in df.keys():\n", + " df[agent_1] = returns[agent_1]\n", + " if agent_2 not in df.keys():\n", + " df[agent_2] = returns[agent_2]\n", + " else:\n", + " for checkpoint in list(returns[agent_1].keys()):\n", + " df[agent_1][checkpoint].extend(returns[agent_1][checkpoint])\n", + " for checkpoint in list(returns[agent_2].keys()):\n", + " df[agent_2][checkpoint].extend(returns[agent_2][checkpoint])\n", + "\n", + "df = pd.DataFrame(df)\n", + "\n", + "df.index = np.array(df.index).astype(int)\n", + "df = df.sort_index()\n", + "# df = df.head(40)\n", + "\n", + "agents = df.columns\n", + "fig = go.Figure()\n", + "\n", + "for agent in agents:\n", + " df[f\"{agent}_avg\"] = df[agent].apply(lambda x: np.array(x).mean())\n", + " df[f\"{agent}_std\"] = df[agent].apply(lambda x: np.array(x).std())\n", + "\n", + " df[f\"{agent}_lower\"] = df[f\"{agent}_avg\"] - df[f\"{agent}_std\"]\n", + " df[f\"{agent}_upper\"] = df[f\"{agent}_avg\"] + df[f\"{agent}_std\"]\n", + "\n", + "\n", + " # fig.add_trace(\n", + " # go.Scatter(\n", + " # x=np.array([df.index, df.index[::-1]]).flatten(),\n", + " # y=np.array([df[f\"{agent}_upper\"], df[f\"{agent}_lower\"][::-1]]).flatten(),\n", + " # fill=\"toself\",\n", + " # fillcolor=\"rgba(0, 0, 255, 0.2)\",\n", + " # line=dict(color=\"rgba(255, 255, 255, 0)\"),\n", + " # name=\"Error Band\",\n", + " # )\n", + " # )\n", + " fig.add_trace(\n", + " go.Scatter(\n", + " x=df.index,\n", + " y=df[f\"{agent}_avg\"],\n", + " mode=\"lines\",\n", + " name=agent,\n", + " )\n", + " )\n", + "\n", + "fig.update_layout(\n", + " title=\"RR returns\",\n", + " xaxis_title=\"Checkpoints\",\n", + " yaxis_title=\"Returns\",\n", + " showlegend=True,\n", + ")\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 66, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(40,)" + ] + }, + "execution_count": 66, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df[\"DR_SP\"].dropna().shape" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": 67, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "('DR_FSP', 'DR_PFSP', 'DR', 'DR')" + ] + }, + "execution_count": 67, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "directory = \"DR_FSP_DR_PFSP\"\n", + "agent_path = directory.split(\"/\")[0].split(\"_\")\n", + "agent_1 = \"_\".join(agent_path[:2])\n", + "agent_2 = \"_\".join(agent_path[2:])\n", + "\n", + "ckp_path = directory.split(\".\")[0].split(\"/\")[-1].split(\"_\")\n", + "ckp_1 = ckp_path[0]\n", + "ckp_2 = ckp_path[2]\n", + "\n", + "agent_1, agent_2, ckp_1, ckp_2" + ] + }, + { + "cell_type": "code", + "execution_count": 68, + "metadata": {}, + "outputs": [], + "source": [ + "def correct_json_format(path: str, overwrite: bool = False):\n", + " \"\"\"Converts a round-robin output to correct json format.\"\"\"\n", + " try:\n", + " with open(path) as f:\n", + " logs = json.load(f)\n", + " return logs\n", + "\n", + " except JSONDecodeError:\n", + " agent_path = path.split(\"/\")[0].split(\"_\")\n", + " agent_1 = \"_\".join(agent_path[:2])\n", + " agent_2 = \"_\".join(agent_path[2:])\n", + "\n", + " ckp_path = path.split(\".\")[0].split(\"/\")[-1].split(\"_\")\n", + " ckp_1 = ckp_path[0]\n", + " ckp_2 = ckp_path[2]\n", + "\n", + " print(agent_1, agent_2, ckp_1, ckp_2)\n", + "\n", + " with open(path, \"r\") as file:\n", + " lines = file.readlines()\n", + " data = [eval(line.strip()) for line in lines]\n", + " assert len(data) == 2, \"More than two entries\"\n", + " new_data = {\n", + " agent_1: {ckp_1: data[0]},\n", + " agent_2: {ckp_2: data[1]},\n", + " }\n", + "\n", + " if overwrite:\n", + " with open(path, \"w\") as output_file:\n", + " json.dump(new_data, output_file, indent=4)\n", + "\n", + " return new_data\n", + "\n", + "\n", + "def merge_logs(dir: str, log_dir: str):\n", + " \"\"\"\n", + " Parses the matchup folder and returns a\n", + " dictionary of returns for each agent/checkpoint pair\n", + " \"\"\"\n", + " agent_path = dir.split(\"_\")\n", + " agent_1 = \"_\".join(agent_path[:2])\n", + " agent_2 = \"_\".join(agent_path[2:])\n", + "\n", + " merged_logs = {agent_1: {}, agent_2: {}}\n", + "\n", + " for file in tqdm(os.listdir(dir)):\n", + " path = os.path.join(dir, file).replace(chr(92), \"/\")\n", + " path = path.replace(\"\\\\\", \"/\")\n", + " try:\n", + " with open(os.path.join(dir, file)) as f:\n", + " logs = json.load(f)\n", + " except JSONDecodeError:\n", + " correct_json_format(path, overwrite=True)\n", + " with open(os.path.join(dir, file)) as f:\n", + " logs = json.load(f)\n", + " for agent in [agent_1, agent_2]:\n", + " for checkpoint, returns in logs[agent].items():\n", + " if checkpoint not in merged_logs[agent].keys():\n", + " merged_logs[agent][int(checkpoint)] = []\n", + " merged_logs[agent][int(checkpoint)].extend([x for x in returns])\n", + "\n", + " with open(\n", + " os.path.join(log_dir, f\"{agent_1}_{agent_2}_returns.json\"),\n", + " \"w\",\n", + " ) as output_file:\n", + " json.dump(merged_logs, output_file, indent=4)\n", + "\n", + " return merged_logs" + ] + }, + { + "cell_type": "code", + "execution_count": 72, + "metadata": {}, + "outputs": [], + "source": [ + "with open(\"parse_results/DR_FSP_DR_PFSP_returns.json\") as f:\n", + " data = json.load(f)" + ] + }, + { + "cell_type": "code", + "execution_count": 114, + "metadata": {}, + "outputs": [], + "source": [ + "def plot_results(data: dict):\n", + "\n", + " LABELSIZE = 36\n", + " TICKLABELSIZE = 24\n", + "\n", + " fig, ax = plt.subplots(figsize=(7 * 2, 3.4 * 2))\n", + " color_palette = \"colorblind\"\n", + " color_palette = sns.color_palette(\"deep\", n_colors=len(data.keys()))\n", + " colors = dict(zip(data.keys(), color_palette))\n", + "\n", + " for alg in data.keys():\n", + " df = pd.DataFrame(data[alg])\n", + " df.columns = [int(col) for col in df.columns]\n", + " df = df.T.sort_index()\n", + " avg = df.mean(axis=1)\n", + " std = df.std(axis=1)\n", + " plt.plot(\n", + " df.index,\n", + " avg,\n", + " color=colors[alg],\n", + " marker=\"o\",\n", + " linewidth=2,\n", + " label=alg,\n", + " )\n", + " plt.fill_between(\n", + " df.index,\n", + " y1=avg + std,\n", + " y2=avg - std,\n", + " color=colors[alg],\n", + " alpha=0.2,\n", + " )\n", + "\n", + " plt.legend(\n", + " loc=\"lower center\",\n", + " ncol=len(data.keys()),\n", + " bbox_to_anchor=(0.5, 1.0),\n", + " # bbox_transform=fig_sample_efficiency.transFigure,\n", + " fontsize=30,\n", + " )\n", + " ax.spines[\"right\"].set_visible(False)\n", + " ax.spines[\"top\"].set_visible(False)\n", + " ax.spines[\"left\"].set_linewidth(2)\n", + " ax.spines[\"bottom\"].set_linewidth(2)\n", + "\n", + " ax.tick_params(length=0.1, width=0.1, labelsize=TICKLABELSIZE)\n", + " ax.spines[\"left\"].set_position((\"outward\", 10))\n", + " ax.spines[\"bottom\"].set_position((\"outward\", 10))\n", + " ax.set_xlabel(\"Steps\", fontsize=LABELSIZE)\n", + " ax.set_ylabel(\"Mean Episodic Return\", fontsize=LABELSIZE)\n", + " fig.tight_layout()\n", + " ax.grid(True, alpha=0.2)\n", + "\n", + " fig.show()\n", + "\n", + " fig.savefig(f\"_mean.png\", bbox_inches=\"tight\")" + ] + }, + { + "cell_type": "code", + "execution_count": 119, + "metadata": {}, + "outputs": [], + "source": [ + "def sort_and_trim_dicts(data):\n", + " min_length = min(len(sub_dict) for sub_dict in data.values())\n", + "\n", + " def sort_and_trim(sub_dict, N):\n", + " sorted_keys = sorted(sub_dict.keys(), key=lambda x: int(x))[:N]\n", + " return {k: sub_dict[k] for k in sorted_keys}\n", + "\n", + " trimmed_data = {\n", + " key: sort_and_trim(sub_dict, min_length) for key, sub_dict in data.items()\n", + " }\n", + "\n", + " return trimmed_data" + ] + }, + { + "cell_type": "code", + "execution_count": 132, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "DR_FSP_DR_PFSP_returns.json\n", + "DR_SP_DR_FSP_returns.json\n", + "DR_SP_DR_PFSP_returns.json\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "C:\\Users\\ryanp\\AppData\\Local\\Temp\\ipykernel_145772\\114467854.py:53: UserWarning:\n", + "\n", + "FigureCanvasAgg is non-interactive, and thus cannot be shown\n", + "\n" + ] + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAABWsAAAKeCAYAAADTKEnNAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/SrBM8AAAACXBIWXMAAA9hAAAPYQGoP6dpAAEAAElEQVR4nOzdd5hkZZk3/u9JFbu6e2Z6mBlyEkSikoPAEkyICiwsigKrK/Ju0J+uiq4KyLorimlfw4q4C7wiJnRlWWQlI0EJAyiSw+TpVLlOTs/vj+rumZpOVdWnUvf3c119TdfpqnOeqa5+6tR97ue+JSGEABERERERERERERF1lNzpARARERERERERERERg7VEREREREREREREXYHBWiIiIiIiIiIiIqIuwGAtERERERERERERURdgsJaIiIiIiIiIiIioCzBYS0RERERERERERNQFGKwlIiIiIiIiIiIi6gIM1hIRERERERERERF1AbXTAyCi7hcEAYIgQBiGnR4KEREREREREc1BlmWoqgpZZo5mL2KwloimEULAsizouo5KpQLXdTs9JCIiIiIiIiJqQDqdRiaTQSaTgaoyBNgrJCGE6PQgiKh7+L6PTZs2wbZtqKqKvr4+pNNpKIoCWZYhSVKnh0hEREREREREMxBCIAxDuK6LSqUCwzAgSRJ22203pNPpTg+P6sBgLRFNcRwHmzZtghACu+yyC5LJJIOzRERERERERD3K930MDw/DMAwGbHsEi1cQEYDq1beNGzdCkiTsueeeSKVSDNQSERERERER9TBVVbHLLrsgnU5j06ZN8H2/00OieTBYS0QAAF3X4fs+dtllF2ia1unhEBEREREREVEEZFnGmjVrIIRApVLp9HBoHgzWEhEAoFQqIZFIIJFIdHooRERERERERBQhVVWRTqcZrO0BDNYSEcIwhK7rGBgY6PRQiIiIiIiIiKgFMpkMDMNAGIadHgrNgcFaIoLv+xBCIB6Pd3ooRERERERERNQCsVgMAFi3tssxWEtEU1fVZJlTAhEREREREdFiNPmZn5m13Y2RGSKaIklSp4dARERERERERC3Az/y9gcFaIiIiIiIiIiIioi7AYC0RERERERERERFRF2CwloiIiIiIiIiIiKgLMFhLRERERERERERE1AUYrCUiIiIiIiIiIiLqAgzWEhEREREREREREXUBBmuJiIiIiIiIiIiIugCDtURERERERERERERdQO30AIiIiKh1giDAs88+i2eeeQb5fB7lchmapqGvrw+77LIL9t57b+y3336Ix+OdHioREUVACIGXXnoJTz/9NLLZLEqlEhRFQTqdxpo1a6bm/XQ63emhEhF1Bcuy8OSTT+Lll19GoVCAYRhIJpPo7+/H7rvvjn322Qd77703ZJn5jtQeDNYSEVHd9txzT2zYsGHO+8TjccTjcaxYsQKrV6/G6173Ohx44IE4/vjjcdRRR0HTtKaOffLJJ+OBBx6Y8z6SJCGTyWBgYAB777033vSmN+Ftb3sbTjvttK45uarnOZzPxz72MXzrW9+a8z5PPvkkvvvd7+LnP/85dF2f876apuGQQw7Bcccdh7e//e04+eSTkUwmZ73/+vXrsddee807Tk3TMDg4iKGhIRx66KE49thjcf7552OnnXaa97FEND/OyQtXz3MoyzIGBgawbNkyHHDAATjyyCNx7rnn4g1veENdx6jnuZrPu9/9bvz617+e8z4vvfQSvve97+Gmm25CLpeb876KouCAAw7AscceO/U76e/vn/MxkiTNO05VVTEwMIDly5fj4IMPxlFHHYXzzz8fe+yxx7yPJaL5cd6PjhAC//3f/43vf//7uPvuu+H7/pz3z2QyOPzww3HSSSfh7W9/O4488sg5/0833HAD/vqv/3recSSTSQwODmLNmjVT+z/nnHOQSCQa/j/RIiKIaMmzLEs899xzwrKsTg+Futwee+whADT9NTg4KD74wQ+Kp556quFjn3TSSU0fd8899xS//vWvo39CmrDQ5xCA+NjHPjbr/g3DEH//938vJElqev//3//3/835f1i3bl3T+47FYuLDH/6wKJVKET+zREsP5+SFW8hzeOqpp4qXXnpp3mMs5Lma/Hr3u9896/593xdf/OIXhaZpTe//Pe95z7z/j2b3Lcuy+Mu//EsxPDzcyK+GiGbAeT8a69evF6eccsqCnsv5/j/XX3990/tevny5+NrXviaCIIj8/87P/r2huy5tEBHRolYsFvGf//mfeOMb34hzzz0XW7Zsactx169fj/e85z34zGc+05bjdYpt2zjzzDPxne98B0KImp9JkoQ999wTb3rTm3DEEUdg7733nrX0wY6PjZLrurjuuutw6KGHLjjDmIgWhnPywtxzzz047LDDcMcdd3RsDGEY4qKLLsIVV1wBz/Om/XzXXXfFYYcdhqOOOgr77rvvrKUPWjnvh2GIW265BQcddBDWrl3bsuMQ0fw47wOvvfYajj/+eNx7773TfhaLxbD//vvjqKOOwqGHHorddttt1uzZVs6b+Xwen/zkJ/GOd7wDruu27DjUvVgGgYiImva1r30Nhx56aM02z/NQKBRQLBaxYcMG/P73v8cTTzwBy7Jq7nfLLbfg/vvvxy9+8QucfPLJDR/7U5/6FN7ylrfUbAvDEKVSCc8//zxuvfVWPPnkkzU//8pXvoJ99tkHH/7whxs+XqvM9BzOZ/fdd59x+0c/+tFpJ57HH388Pv7xj+Ntb3vbtA/pnufhmWeewX333Ydf/OIXeOyxx5o+8bzrrrumbXNdFyMjI3jooYfws5/9DKZpTv1s/fr1eNvb3oYnnniCdROJIsI5eeFmeg6DIEAul8OTTz6Jn/70pzXBDdM0ce655+KRRx7BIYccUtcxZnqu5jNb+Zirr74aP/7xj2u2HXjggfj0pz+NM888E8uWLav5WRiGeO655/Dggw/illtuwQMPPIAgCBoay6SbbroJq1atqtnmeR7Gx8fx2GOP4eabb0ahUJj6WS6XwxlnnIEnn3wSO++8c1PHJKJanPcb43kezjzzzJp5XJIkXHDBBfjIRz6CY445BqpaGybTdR1r167FHXfcgVtuuQWvvvpqU8c+5JBD8PWvf33adl3XsXHjRtx55534zW9+U3Mu/tvf/hYf+tCH8KMf/aipY1IP62RaLxF1By6FoHrtuPTqvvvuq+txpmmKa6+9VhxwwAHTlvkkk0lx//33z7uPHZdeXX/99fM+5qabbhLxeLzmcStXrhSVSqWucbdCs8/hfJ5++mkhy3LNvi+//PKG9vHSSy+Jv/u7vxOf+9zn5rzfTGUQ5jMyMiJOPvnkaY/70pe+1NAYiWgbzskL1+hz6DiO+NjHPjbteTvttNNmfUwzz1U9hoeHRSqVqtn3X//1Xwvf9+vex5YtW8RnP/tZ8cEPfnDe++74f163bt2c9y+Xy+K8886b9ri/+Zu/qXt8RFSL8/7C/N//+39rxpJIJMTtt9/e0D7uv/9+8e53v1v893//95z327EMwkknnTTvvh977DGx2267TfsdPfTQQw2NcS787N8bWAaBiIhaLplM4pJLLsGf/vQnfPzjH6/5mWVZOPfcczE8PBz5cS+44AJ84xvfqNk2Pj4+b5OWXvSjH/0IYRhO3X7rW9+KL37xiw3t43Wvex2+853v4Etf+lLUw8OqVatw2223Yf/996/Z/m//9m8tXUZGRNNxTm5eLBbDt771LVxwwQU12++++278+c9/butYbrnllpoVCwcddBB+8IMfQFGUuvex884741//9V/xH//xH5GPL5PJ4Oabb8ZJJ51Us/2GG25AsViM/HhENDvO+1U33nhjze0rrrgC73jHOxrax0knnYRf//rXOPPMM6McGgDgyCOPxB133DGtVNk3v/nNyI9F3Y3BWiIiahtVVfGNb3xjxpO2T33qUy055iWXXDJtueU999zTkmN10o5lCD7ykY90aCSz6+vrw+WXX16zbXx8HE899VSHRkS0tHFObt7VV189rY7hnXfe2dYx7Djvf/CDH5y2fLfTFEXBv/7rv9Zs831/xlqRRNR6S3nez+fzNXWzZVnuqjI8kw488EBceOGFNdvuueeemqQMWvwYrCUiorb7+Mc/jrPOOqtm280334wXX3wx8mOpqjqtDtcrr7wS+XE6bfPmzTW3X//613doJHN7xzveAUmSarY988wzHRoNEQGck5ux6667TqsT2e65rFfm/WOPPRYrVqyo2cZ5n6izluK8v2MztaGhoWlzU7d45zvfWXO7WCxi06ZNHRoNdQKDtURE1BFf+9rXarKShBC49tprW3KsXXfdteZ2NpttyXE6qVKp1NxutmFMqw0ODmL58uU12xbj74Oo13BObtzee+9dc7vd/49emfclScKee+5Zs61Xf+dEi8lSm/d7Zc4Epr+/AJw3lxoGa4mIqCP23nvvabWeWlW/aseaqDtmdi4Gg4ODNbf/8Ic/dGYgddA0rea27/sdGgkRTeKc3LhOz2Wc94loIZbavL/jnJnL5bp2ZceOcybAeXOpYbCWiIg65uyzz665vW7dOmzYsCHy4+y4VHTVqlWRH6PTDjzwwJrbV199NfL5fIdGMzvXdZHL5Wq2desSNKKlhnNyY7Zu3Vpzu91z2Y7z/r//+79j/fr1bR1DvTr9XBHRzJbSvL/33nsjkUjUbLvsssu6stHtjnMmwHlzqWGwloiWjCAUeOaVLB54cjOeeSWLIOy+N+al5uijj562LepmU77v4/7776/Zdvjhh0d6jG5wxhln1Nx+9dVXccQRR+BnP/sZPM/r0Kime/jhh6eNZ6+99urQaKiTRBjA2vBn6M8+CGvDnyHC7l2OuFRwTq6faZp4/PHHa7a1ey7bcd7P5/M46qij8IMf/ACmabZ1LHNZt24dNm7cWLON8/7SFIYhnh17CQ9teBzPjr3EhkldYCnN+4lEAqeeemrNtl/96lc49dRT8fDDD7d9PHPZ8fmKxWLYZZddOjMY6ojuahdKRNQij/xpK37w62eQK9lT21YMJHDJew7GcYfsPMcjqZX2228/9PX1Qdf1qW2vvfZapMe49tprMTw8XLPtvPPOi/QYC7F27dqGljUdfvjhWLZs2bTtl1xyCa6++uqarNV169bh/PPPx+DgIN7ylrfguOOOw9FHH43DDjtsWmZBu1xzzTU1t+PxOI477riOjIU6x3jhD8je+Z8IKtter0pmBYbe8kGkX39MB0e2tHFOrt+3v/1tWJZVs+2UU06p67HPPfcc7r777rqPdeCBB2LNmjXTtp911lnYf//9axoCjY+P4yMf+Qg+/vGP49RTT8UJJ5yAo48+Gm9605uQyWTqPmaUvvrVr07bVu9zRYvHo5ufwg1P/hw5qzi1bUVyEBe/6TwcvesbOzewJW6pzfuf+cxncPvtt9dsu++++3DCCSdgjz32wFve8hYce+yxOOqoo3DAAQfU1PRtl1KpNK128DHHHINkMtn2sVDnMFhLRIveI3/aii/f+Pi07bmSjS/f+Dg+e9GRDNh2iCRJWLFiRc0J4o4ncwvx4x//GP/4j/9Ys+2MM87AUUcdFdkxFuqTn/xkQ/e/7777pnXUBYD+/n7cfPPNOOOMM6YFf4vFIn7+85/j5z//OYBqV95DDz0UJ510Ek477TScfvrpUNXWnhIIIfC5z30Od9xxR832c889lyefS4zxwh8w+strpm0PKjmM/vIarDrnUwzYdgjn5Prcdttt+MIXvlCzba+99sIJJ5xQ1+OvueaaaReu5nL99dfj4osvnrZdURT87Gc/wwknnFDzOwOqmb+33XYbbrvtNgCALMs44IADcNJJJ+GUU07BO97xjrbMvd///vfx/e9/v2bbCSecwMzaJebRzU/h6w//YNr2nFXE1x/+Af7x+EsYsO2QpTbvn3DCCfjCF76Af/7nf572sw0bNuC6667DddddBwDo6+vDUUcdhZNPPhlvf/vbccQRR7R8fJVKBeeddx5GR0drtl944YUtPzZ1FwZriWhRC0KBH/z6mTnvc92tf8bRB62BIvdmg5NeNzg4WFMba8cPnLOZKTNJCIFSqYTnn38et956K9auXVvz89e97nW4/vrrFz7oLvWWt7wFd911Fy644IIZa11N8n0fa9euxdq1a/GNb3wDq1atwoc//GF8+tOfjjTzynVdjI6O4qGHHsK3v/1t/P73v6/5eTKZxJVXXhnZ8aj7iTBA9s7/nPM+2bv+E6n9joQkK20aFW2Pc/J0QRCgUChg7dq1uPHGG/HTn/50Wo3DL33pSzM2hGm1Qw89FI888gjOPffcmgzbHYVhiGeffRbPPvssvve972FgYAAXXnghPv/5z2OnnXaKbDy+72N8fByPPvoofvCDH0y7QCfLMr785S9HdjzqfmEY4oYnfz7nfW548hc4cudDO5LFSEtv3r/qqqswNDSEyy67DLZtz3o/Xddx77334t5778Xll1+OAw88EJ/4xCdw8cUXR/paNU0TGzZswJ133olvfvOb02oGH3DAAbjooosiOx71BgZriSgSH//m/ShUnE4PYxrPD1A25q7XmS1auPDKO6Cp3RUYWJaJ45sfP7nTw2i5vr6+mtuu69b1uEYzk84//3x897vfxfLlyxsaX685+eST8fLLL+Pb3/42vv/979fVbGZ0dBRf+tKXcN111+HHP/7xtHpe9Wi0q68sy7j55puxzz77NHwsmt/m//g0AqPQ6WFMI3wPoVWZ8z5BOYcN3/oQJLX9ga+5KOll2PVD05dzLzZLfU7+i7/4i4Yf84lPfALve9/7WjCa+hx88MF45pln8B//8R/49re/jeeee27ex5RKJXz729/GjTfeiO9///t473vf2/Bxm8mO/da3vlV3BjI15jN3fhlFu9zpYUzjBR4qrjHnfXJWAR++9dPQlO6a9wcT/bj6LZ/t9DBabinO+x/96Edx9tln48tf/jJ+/OMfo1QqzfuYZ599Fh/60Ifwve99D7/85S+xxx57NHTMBx54oOHz5ZUrV+K2225r+Qo46j78jRNRJAoVp6YebK+pBnS7pwnTUlKp1AZu4vF4pPtPp9P46U9/ine+852R7jcqs5U1WIhUKoXLLrsMn/70p/H73/8ed911Fx588EE8/vjjKJdn/yA3OjqKt73tbfj5z3+Os846K9IxbW/PPffEDTfcgJNOOqllx1jqAqOAoJLv9DCaNl9Al1pnqc/JjUin07jmmmvwf/7P/2nocbOVNVgITdNw6aWX4tJLL8XTTz+N3/72t/jd736HRx99tKaW+Y7K5TLe9773IZvN4h/+4R8iHdP2hoaG8P3vfx/nnHNOy46x1BXtMvLb1YPtNfMFdKl1luq8v+uuu+K73/0uvvGNb+DOO+/EfffdhwcffBB/+tOf5gxYr127FkcddRQeeeSRliYdnHzyybjhhhsaDgrT4sBgLRFFYlkm2jf1qNSTWQsA/WmtKzNrl4Idr2TveHV/oQzDwF/+5V/ihz/8Id7//vdHuu9uJ0kSjjvuuKkGXkIIvPzyy3j00Udx77334tZbb0WhUJt96fs+LrroIhx22GGR1hRcvnw5jj76aLz//e/HOeecE/kHAaqlpKc3oesG9WTWAoCczHRlZu1SwDl5bolEAm984xtx9tln4+KLL8bQ0FCnhzTNYYcdhsMOOwyXXXYZAGD9+vV47LHHcP/99+PXv/71jPUoP/GJT+DII4/EMcdEVy86k8ng8MMPx/nnn48LLrgg8tcS1RpM9Hd6CDOqJ7MWADKxdFdm1i4FS33ej8fjOPPMM3HmmWcCqGYWP/PMM3j44Ydx991347e//e204O3Y2BjOOeccrF27FooS3WfI3XffHSeeeCI+9KEPRZ7MQb2FwVoiikS3LtcPQoEPfenOObN+hwaT+OHnTmfN2g4QQiCbzdZs23nn+pq9zZSZZJomNm7ciHvuuQdf//rXsW7dOgCA4zi46KKL0N/fj3e9612RjL0XSZKE/fbbD/vttx8+8IEPwHVd3HDDDfjsZz+LfH5bFmalUsG//uu/TjVYqMddd901bZumaRgYGMCKFSuw2267RfJ/oPp063J9EQbY+J3/g6Aye6af0r8Cu//dv7NmbQdwTga+9rWv4dBDD63ZpigKMpkMBgcHseeee/bcctQ999wTe+65J8477zx85zvfwS233IJPfepT2Lhx49R9fN/HF77whRnn8tncdNNNWLVqVc02VVXR39+PZcuWYY899mAN0jbq1uX6YRji7/7nc8jNkfW7IrkM333nl/h66QDO+9PFYjEcfvjhOPzww/HRj34UuVwOX/nKV/CNb3wDQRBM3e+Pf/wjfvrTn+KCCy6oa7+HHHIIvv71r9dskyQJiUQCAwMDWL16dVdeAKTO6K0zDSKiBimyhEveczC+fOPjs97nw+8+iIHaDnnhhRdgGLXZFgtZTpRKpfD6178er3/963HxxRfjPe95z1TjgzAMcdFFF+HZZ5+t+yR0sYvFYrjkkktw+umn47jjjsPIyMjUz37605/i3//93+sOSpx22mmtGiYtIpKsYOgtH8ToL2evcTd0+gcZqO0QzsnA4YcfvqizmWRZxnnnnYdTTz0Vb37zm/H8889P/ezuu+/GyMgIVq9eXde+jj/+eOy5554tGiktFrIs4+I3nYevP/yDWe9z8ZvOZaC2Qzjvz2/FihX46le/ihNPPBHvec97agK2N910U93B2mXLlvF8merGGZGIFr3jDtkZn73oSKwYSNRsHxpM4rMXHYnjDumdk4XF5rHHHpu27Y1vfGMk+06n0/jlL3+Jfffdd2pbsVjExz72sUj2v5jstdde+OpXazMxdV3H008/3ZkB0aKWfv0xWHXOp6BkVtRsV/pXYNU5n0L69dEtw6bGcE5eOlasWIFrr7122vaHHnqoA6Ohxe7oXd+Ifzz+EqxIDtZsX5Fchn88/hIcvWs08ww1jvN+/d75znfioosuqtnGOZNahZm1RLQkHHfIzjj6oDV47rUc8mUby/sTeMPeK5hR22G33HJLze19990Xu+66a2T77+/vx7XXXotTTz215piPPPLIVB1Xqjr77LPxwQ9+EL7vT23bsmULjjjiiA6Oihar9OuPQWq/I2Fveh6BXoDStwyJ3Q5gRm2HcU5eWt785jdjzZo1NTVst2zZ0sER0WJ29K5vxJE7H4rns6+gYJWwLDmAA4b2ZUZth3Heb8x5552H//zP/5y6res6SqUSBgYGOjgqWow4MxLRkqHIEg7edwgnvWlXHLzvEAO1Hfbaa6/hN7/5Tc22s846K/LjnHLKKdO6z37hC1+I/Di9Lp1OT6uTZZpmh0ZDS4EkK0jucRD6DnwzknscxEBth3FOXpp27DLOeZ9aSZZlHLjTfjhhjyNx4E77MVDbYZz3GzdT6RfOm9QKnB2JiKgjPvnJTyIMw6nbsizjkksuacmx/vmf/7nm9r333otHHnmkJcfqZTvWLGOTA6Klg3Py0sR5n2jp4rzfuB3nTKBaVoYoagzWEhFR233zm9/Ef/3Xf9Vsu/DCC2tqWkXpsMMOw5lnnlmz7Ytf/GJLjtWrXnzxRVQqlZptu+++e4dGQ0TtxDl5aSqXy3jppZdqtnHeJ1oaOO835/HHa5tWr169GrFYrEOjocWMwVoiImob3/fxj//4j/jEJz5Rs3316tX4yle+0tJjX3755TW377zzTjz66KMtPWY7/fCHP4Su600//utf/3rN7T333BP777//QodFRF2Mc3Jvu+mmm5DNZpt+/He+8x04jjN1O5lM4sQTT4xiaETUpZbyvJ/NZnHzzTfXZBM3wnVdfPvb367Z9ta3vjWKoRFNw2AtERG1nG3buO6663DIIYfgG9/4Rs3PUqkUbrnlFuy0004tHcMRRxyBt7/97TXbrrrqqpYes52+9KUvYa+99sLVV1+NsbGxhh779a9/Hdddd13Ntg984ANRDo+Iugjn5MXhhz/8Ifbaay989rOfxYYNGxp67E9+8hNceeWVNdv+8i//EslkMsIRElG34LxfbQZ2wQUX4OCDD8ZNN90Ey7Lqfqxt23j/+9+PZ599tmb7hRdeGPUwiQAAaqcHQEREvWvt2rXwfb9mm+d5KBaLKBaLWL9+Pf7whz/giSeemLH4/qpVq3DLLbfg+OOPb8t4L7/8ctxxxx1Tt3/zm99g7dq1OPzww9ty/FbLZrP47Gc/iy984Qt4+9vfjne/+904/vjjsf/++0OSahvqVSoV3Hnnnfi3f/s3PPjggzU/22233XDZZZe1c+hEFAHOyUuPruu4+uqr8ZWvfAUnn3wyzj77bJxwwgk45JBDpjVvsiwLDzzwAL73ve/htttuq/lZX18frr766nYOnYgiwHm/cc899xw+8IEP4O/+7u9w3nnn4S1veQtOOOEErFmzZtp9t27dil//+te45pprsH79+pqfnX322TjllFPaNGpaahisJSKipn3yk59s+rHnn38+vvnNb2L16tURjmhuxxxzDE477TTcfffdU9uuuuoq3HrrrW0bQzv4vo/bbrtt6sN4X18fdtppJ6xYsQJBECCXy2Hjxo0QQkx77MqVK/Gb3/wG6XS63cMmogXinLx0CSFw33334b777gNQLWmwatWqqYZhhUIB69evRxAE0x6bSqXw3//939h5553bOmYiWjjO+80rl8v44Q9/iB/+8IcAqo3ChoaGMDg4CNu2MTw8POtqtaOPPhrXX399O4dLSwzLIBARUdssX74cH/7wh/HMM8/gJz/5SVtPDiftWC/rtttuwx//+Me2jyNqxxxzDFR15muwuq7jtddew+OPP44nn3wSGzZsmDFQe8opp+CRRx7BQQcd1OrhElEX4Jzc24444ggkEokZf2ZZFtavX48nnngCTzzxBF599dUZA7VvfOMb8cADD+Av/uIvWj1cIuoCS3ne7+vrw6GHHjrrz3O5HF588UU8+uij+OMf/zhjoFaWZVx66aW466670N/f38rh0hLHzFoiIopULBZDIpHAihUrsHr1arzuda/DQQcdhOOPPx5HHnkkNE3r6Pje/OY34+STT8b9998PoJqNdNVVV+GXv/xlR8e1UD/96U+Rz+dx22234e6778bvfvc7bNy4cd7HpdNpvPOd78RFF100rY4YEfU+zsmL19e+9jVceeWVuOOOO3DnnXfid7/7HV566aV5HxePx3H66afjwgsvxNlnnw1FUdowWiJqF877MxsaGsLTTz+N1157Dbfeeivuu+8+PPTQQygUCvM+ds2aNTjvvPPwoQ99CAcffHBLx0kEAJKYKbWGiJYU27axbt067LXXXrNmaBBR7xkfH8eLL76I1157Dfl8HrquIxaLob+/H0NDQzjooIOw3377TatrSEREvalYLOLFF1/EK6+8glwuh0qlAkVR0N/fj+XLl+MNb3gDDjjggI4Ha4iIuoEQAhs2bMBLL72EjRs3olQqwbIspFIpZDIZ7Lzzzjj00EOx6667dnqokeFn/97AYC0RccImIiIiIiIiWuT42b83MJWGiIiIiIiIiIiIqAswWEtERERERERERETUBRisJSIiIiIiIiIiIuoCajsOsnnzZtx1111Yu3YtXnzxRZRKJZRKJXie1/Q+JUnCq6++GuEoiYiIiIiIiIiIiDqnpcHaxx9/HFdccQXuuusuhGE4tT2KnmaSJC14H0RERJMKhQLWrl0b+X4PPPBArFmzJvL9EhEtZpyTiYiWFs77RNu0LFh7+eWX4+qrr0YQBNOCswsNtEYR7CUiItreH//4R5x++umR7/f666/HxRdfHPl+iYgWM87JRERLC+d9om1aEqz9whe+gH/5l3+Zur1jcJbBViIiIiIiIiIiIqJakQdrH330UfzLv/zLjAFaRVHwute9Dvvssw8GBgagaVrUhyciIiIiIiIiIiLqSZKIOM319NNPxz333DMVrBVCYNWqVbjiiitw/vnnY3BwMMrDEVEEbNvGunXrsNdeeyGRSHR6OEREREREREQUMX727w2RZtYWi0Xcf//9kCQJQghIkoTDDz8cd955J5YtWxbloYiIiIiIiIiIiIgWFTnKnT300EMIgmDqdiKRwC9/+UsGaomIiIiIiIiIiIjmEWmwduvWrVPfS5KEs846C7vvvnuUhyAiIiIiIiIiIiJalCIN1uZyOQDVOrUAcPzxx0e5eyIiIiIiIiIiIqJFK9JgbTwer7m9cuXKKHdPREREREREREREtGhFGqzdZZddam7ruh7l7omIiIiIiIiIiIgWrUiDtYcffjiAar1aANi4cWOUuyciIiIiIiIiIiJatCIN1u6777446KCDpm7ffffdUe6eiFpsst40ERERERERES0u/MzfGyIN1gLAxz/+cQghIITAI488gj/+8Y9RH4KIIibL1akgDMMOj4SIiIiIiIiIWmHyM/9kDIC6U+S/nYsvvhjHHHMMgGrE/m//9m/hum7UhyGiCKmqCkmS4DhOp4dCRERERERERC0wGZ9TVbXDI6G5RB6slSQJv/jFL7DzzjtDCIE//OEPOO+881CpVKI+FBFFRJZl9PX1oVQqdXooRERERERERNQClUoF6XSambVdriW/nV122QUPPvgg9ttvPwghcNttt+Gwww7Dj370I/i+34pDEtECDQwMwLZt2Lbd6aEQERERERERUYR834dhGMhkMp0eCs1DEhFXF77qqqumvi+Xy/je974Hx3EghIAkSUin0zjmmGPwute9DsuXL4emaU0f6/LLL49iyESEatmSV155BbIsY/fdd1/Q3yYRERERERERdYcwDLFlyxYYhoF9992XZRC6XOTBWlmWIUnSjD+bPNRsP29UEASR7IeIqlzXxcaNGyGEwC677IJkMhnZ3ysRERERERERtZfv+xgeHoZhGNhtt92QTqc7PSSaR8uCtTvudvuATxSHlCSJwVqiFvB9H5s2bYJt21AUBX19fUin01BVdc6LMURERERERETUWUIIhGEI13VRqVRgGAYkSWKgtoe0NbM2KpMlFRisJWoNIQQsy4Ku69B1HY7jdHpIRERERERERNSAdDqNTCaDTCbD0gc9pCXB2nZgsJaofYIgQBAECMOw00MhIiIiIiIiojnIsjy1OpZ6T+TBWiIiIiIiIiIiIiJqHEPsRERERERERERERF2AwVoiIiIiIiIiIiKiLhBpdeF8Po8///nPNduOPfZYaJoW5WGIiIiIiIiIiIiIFp1Ig7W/+tWv8JGPfGTq9t57742XX345ykMQERERERERERERLUqRlkHIZrMQQmCyZ9k555wT5e6JiIiIiIiIiIiIFq1Ig7WTQVpJkgAAe+21V5S7JyIiIiIiIiIiIlq0Ig3WDgwMANgWtF2xYkWUuyciIiIiIiIiIiJatCIN1k5m0k5m1maz2Sh3T0RERERERERERLRoRRqsPeKII6YCtQDwwgsvRLl7IiIiIiIiIiIiokUr0mDtypUrcfTRR081Gbv99tuj3D0RERERERERERHRohVpsBYA/v7v/37q+9deew0333xz1IcgIiIiIiIiIiIiWnQkMdkNLEInnXQSHnzwQQDA0NAQHnzwQey///5RH4aIiIiIiIiIiIho0Yg8sxYAfv7zn081G8tms/iLv/gL3HHHHa04FBEREREREREREdGi0JJg7apVq/DII4/gzW9+MwBgZGQE73znO3HaaafhJz/5CUZGRlpxWCIiIiIiIiIiIqKeFXkZhFNOOWXq+zAM8fDDDyMMQwghIEnS1M9WrVqFnXbaCf39/VBVteHjSJKEe+65J5IxExEREREREREREXVa5MFaWZZrgrLbm+lQs913LpOB3yAIGn4sERERERERERERUTdqPKW1ATtm0zYTmCUiIiIiIiIiIiJaClpSsxbYlkUrhIj8i7rbEUccgV133RVHHHFEp4dCRERERERERETUMyLPrD3xxBOZQbvEjYyMYMuWLZ0eRlcRQqBUKmFgYIB/H1QXvmaoGXzdUKP4mqFm8HVDjeJrhprB1w01iq8ZakY3vm4iD9bef//9Ue+SiIiIiIiIiIiIaNFrWRkEIiIiIiIiIiIiIqofg7VEREREREREREREXYDBWiIiIiIiIiIiIqIuwGAtERERERERERERURdgsJaIiIiIiIiIiIioCzBYS0RERERERERERNQFGKwlIiIiIiIiIiIi6gJq1Dv83e9+F/UuZ3XiiSe27VhERERERERERERErRR5sPbkk0+GJElR73YaSZLg+37Lj0NERERERERERETUDpEHaycJIVq1ayIiIiIiIiIiIqJFp2XB2lZm1zIQTERERERERERERItNS4K1UQVTJwO+DM4SERERERERERHRYhd5sPa+++5r+rGe5yGXy+GVV17BAw88gHvvvRdhGEKS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DMWAKquwnCS0PUE1z/PgwYNP5ZfhkUzyh9PZtZIvlNOynucC8HJejoYPXdYgCxJs14YsFufOsS4rBlWS0RJoHNEXliIhA1t3ReEzVfj04XeXXjigYfPOLti2i46YhTENgWHRQK0aFHxvevXVV/HII4+kg6yCIODUU0/Fn//8Z9TX1wMAFi9ejG3btqWXueGGG3qtw3VdPPbYY/jNb36D5557DoIgoL29HX//+9/x97//HWPHji30Zpfc6aefjhkzZuD5559HIpHAGWecgUceeQRTp05Nz7Nz506cd955+OijjwCksmqvu+66fte5bt26Xg3B5s+fj0svvbTPfJ7n4atf/SoefPBBAIAoirjrrrvwhS98oUD/OyKi8hFVAwIEeK4LYZCTR8912FyMiIjKQpUUmIqBqBVNB2stJwlVUkbkLcX0mXTt2s251671rDgE1YDEEggjliarUGUVCTsJWS18ANFxHUTtGMaHRsMc4QkPhiajMeKDacjDomTn3vyGAr8hY2d7DKauoD48sl/vUir4O/fmm29OPxYEAYcffjgefvhhqGr2t2CIooi5c+di7ty5+OMf/4hrr70W8Xgc77zzDmbMmIGXX34Zzc3Nhd70krv77rtx5JFHYvPmzVi3bh0OOeQQnHDCCZg4cSK2b9+OpUuXIhqNAvgs8zUcDg953D/84Q/485//nH4+ceJELFu2DMuWLctq+VtuuWXI20BEVCyiZkBQNHjJBARt4BMKz7YgSCpEhbcJEhFRaaVqSwbQGm9LT0vYCZiqCU3i59JIl8quDcGNd0Iys8+SdRMxyJEmCCVoLkWVSRIl+FUTu6K7ARQ+uNYa70BED6POFyn4uqvRqIbhe2FEFAXUh020d1lojJjQteGXPVypCvqX9jwPS5Ys6ZVV+9///d85BWr3dvnll2P06NE488wzkUwm8cknn+Ccc87JOrBYyUaPHo2nn34a5513Ht544w14nodnn30Wzz77bK/56uvrMX/+/F51boeiZ1YzAKxevRqrV6/OenkGa4mokgmSAskIwO7cBXHQYG0SombyCw0REZWFqRoQBBGu50IURFiujSYtMCwztCg3n2XXfpR1dq3nOoAAyDkEd2l48qkmtnXtLPh6o8k4ZElGc7ABUg4Z31S9gn4V9TUm6phVW1IFLS7y9ttvo63tsyvDBxxwAI4++ughr/fU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ERER0TDV4K/DuPAoSKJU7k0hogEUvGbt7t278f/+3//rlUk7evRo3HvvvZg+fXp6vp/85Cc5rXfu3LlQVRXJZBKe5+Gpp54q5GYTEREREREREQ1bqqSUexOIKAsFz6y9+eab0d7eDiAVqG1sbMRLL73UK1CbD7/fj4MOOij9fNOmTdixY8eQ1klERERERERERERUKQoerL3//vvTpQ8EQcCtt96KUaNGFWTd06ZN61XrdtWqVQVZLxEREREREREREVG5FTRYu27dOqxZsyb9fP/998e//Mu/FGz9kyZN6vV87dq1BVs3ERERERERERERUTkVNFj7xhtvpB8LgoBTTz21kKtHOBzu9by73AIRERERERERERFRtStosHb79u0AkC5VMGXKlEKuHqFQCADSzcs6OjoKun4iIiIiIiIiIiKicilosHbnzp29nu+dCTtU0Wi013NFYSdDIiIiIiIiIiIiGh4KGqz1+Xy9nu8dXB2q7mBwd+ZubW1tQddPREREREREREREVC4FDdbW19cD+KxMwbZt2wq5erz55pu9njNYS0RERERERERERMNFQYO1TU1NvZ6vWLGikKvHM888kw4EA8ABBxxQ0PUTERERERERERERlUtBg7VHHXUUVFUFkCpVsGTJEliWVZB1P/bYY9iwYUP6eUtLC/bdd9+CrJuIiIiIiIiIiIio3AoarDUMA8cdd1y6puyuXbtw1113DXm9lmXh+uuvB5AKAguCgFmzZg15vURERERERERERESVoqDBWgD4yle+AiBVt9bzPHz/+9/H5s2bh7TOb3zjG3jjjTd6lUC48sorh7ROIiIiIiIiIiIiokpS8GDtvHnz0uUJBEHAzp07MXv27F4lDLLV3t6Os88+G3fccUc6+CsIAo477jhMnz690JtOREREREREREREVDYFD9ZKkoSbbropXQpBEASsWrUKBx54IG688cZBs2xd18Vrr72GH/zgBxg3bhwefvjh9LoAQFVV/PrXvy70ZhMRERERERERERGVVcGDtQBw5pln4vrrr+8VsO3s7MQNN9yA0aNHY8KECVizZk2vIOz06dMxefJkhEIhHH300bjpppvQ1taWzqbt/vc3v/kNDj/88GJsNhEREREREREREVHZyMVa8U9/+lNs3boVt99+OwRBSAdcAWDdunW96s96nodXXnmlV/AWQK95AOC6667DVVddVaxNJiIiIiIiIiIiIiqbomTWdrv11ltxxx13wDCMdGZs9w+APgHbnr/v/p3neVBVFQsWLMAvfvGLYm4uERERERERERERUdkUNVgLpBqOvfvuu7jqqqug6zo8z+uVQbt3cBZAeh5RFHHppZfi/fffx8UXX1zsTSUiIiIiIiIiIiIqG8Hbu/ZAEe3YsQOPPfYYnnvuObzwwgvYuHEjYrFY+veyLKO+vh5HHXUUTj75ZJx++ukYO3ZsqTaPCmT06NH49NNPMWrUKGzcuLHcm0NERERERERERFQVShqszSQej2P37t0wDAPhcLicm0IFcvjhh2PLli1oamrC8uXLy705REREREREREREVaHswVoiIiIiIiIiIiIiKkHNWiIiIiIiIiIiIiIaHIO1RERERERERERERBWAwVoiIiIiIiIiIiKiCsBgLREREREREREREVEFYLCWiIiIiIiIiIiIqALIucz8z3/+s1jbkZfjjz++3JtAREREREREREREVBCC53letjOLoghBEIq5PVkTBAG2bZd7M4iIiIiIiIiIiIgKIqfM2m45xHeJiIiIiIiIiIiIKAt5BWvLnV3LYDERERERERERERENN8yspYpx+OGHY8uWLeXeDCIiIiIiIiIiooJramrC8uXLB5wnr2Btt9raWnz5y19Gc3PzUFZDBADYsmULPv3003JvBhERERERERERUVkMucGYJEk45ZRTcNlll+ELX/gCJEkq+EbSyMDMWiIiIiIiIiIiGq6yyazNOVgL9K5Z63le+nldXR0uuugizJs3D1OmTMlnm4mIiIiIiIiIiIhGpJyCtcuWLcMf//hHLFy4EF1dXakV7AnUdq+m+/nhhx+Or371q/jKV76CYDBY6O0mIiIiIiIiIiIiGlZyCtZ26+zsxL333ov58+fjpZdeSq1or6Bt9zRd13H22Wdj3rx5mDVrVoE2m4iIiIiIiIiIiGh4yStY29OqVatwxx134C9/+Qu2bt2aWmk/2bbjx4/HvHnzcMkll2DMmDFDGZaIiIiIiIiIiIhoWBGHuoLJkyfjV7/6FTZu3IiHHnoIc+fOhSRJ6Vq2PQO3a9euxQ033IAJEybglFNOwf333w/Lsob8nyAqBsuycNddd2HOnDkYN24cdF1Hc3Mzpk+fjptvvhk7duwYlmNT/srxuq1btw7/93//hwsvvBAHH3wwampqoCgKIpEIpk6diq997Wt47rnnCj4uFUalvde//e1vpz+7BUHA+PHjSzo+ZacS9puVK1fi+9//Pg4//HA0NzdD0zS0tLTgsMMOw2WXXYa77rqLTUMrSDn3mZdeeglXX301DjvsMEQiESiKgmAwiP322w/nnnsu7r77biQSiaKNT7lzHAdvvfUW7rjjDlx11VU4/PDDoapq+rNh5syZRd+GSjjOUW7Kud/wfLg6VcKxJhOeD1e2Stpvino+7BXBli1bvF/+8pfeAQcc4AmC4AmC4Imi6Imi2Od5bW2td80113grV64sxqYQ5eX999/3DjnkEA9Avz8NDQ3eY489NqzGpvyV+nVbuXKld+SRRw44Xs+fmTNneuvXry/I2FQYlfZef+WVVzxRFHuNP27cuJKMTdkr936zdetW74ILLsjquPP1r3+9KNtAuSnXPrNjxw7vX/7lX7LaVyZOnOgtW7asoONTfh566CHPNM0BX68TTjihqNtQ7uMc5a5c+w3Ph6tXJRxrMuH5cGWrlP2mFOfDMoqgsbER3/ve9/C9730PL730Ev74xz/igQceQGdnJ4DeZRJ27dqF3//+9/j973+PqVOn4rLLLsMFF1yASCRSjE0jGtTGjRsxe/ZsbNq0CUBqfz3++OMxceJEbN++HUuXLkUsFsO2bdtw5plnYvHixTjxxBOrfmzKXzletw8++ACvvvpqr2mTJk3CgQceiLq6OrS2tuLFF1/Exo0bAQDPPvssjjnmGDz//POYMGHCkMamoau093oymcTll18O13WLNgYNXbn3mw0bNmDmzJlYu3Ztetr++++Pgw46CLW1tYhGo1izZg3eeOMNRKPRgo1L+SvXPhOLxXDSSSfhjTfeSE+rr6/HoYceitGjR2P79u1499138fHHHwMA1qxZg89//vN4+umncdRRRw15fMpfa2trWd+/5T7OUX7Ktd/wfLh6lftYkwnPhytfJew3JTsfLmx8uX9dXV3en/70J2/GjBn9Ztt2T9N13Tv33HO9J554olSbR5Q2Y8aMXlfR3njjjV6/3759uzd79uz0PJFIxNu9e3fVj035K8frds8993gAvH333df7r//6L2/jxo195nEcx7vjjjt6XX08+uijPdd1hzQ2DV2lvdf/4z/+Iz3W+eefz0yCClXO/aa1tdWbMGFCet2zZs3y3nzzzYzz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ERFRBWCwloiIiIiIiIiIiKgCMFhLREREREREREREVAEYrCUiIiIiIiIiIiKqAAzWEhEREREREREREVUABmuJiIiIiIiIiIhoRPvnP/+JuXPnoqWlBYIg4OGHH855HZ7n4eabb8akSZOgaRpGjRqFn//85zmtQ855VCIiIiKiYcy2bbz77rtYtWoVWltb0draCsdx4PP54Pf7MXr0aIwfPx7jx4+Hpmnl3lwiIiIiKoCuri4cfPDBuOyyy3DWWWfltY5rr70WTz75JG6++WYcdNBB2LVrF3bt2pXTOgTP87y8RiciIiIiGiYSiQQeeugh/OlPf8KyZcsQi8UGXUZRFBx44IE44ogjcMIJJ+Dzn/886urqSrC1RERERFRMgiDgoYcewplnnpmelkgk8MMf/hD33HMPWltbceCBB+KXv/wlZs6cCQB4//33MXXqVLzzzjvYf//98x6bZRCIiIiIaER79NFHse++++K8887DkiVLsgrUAkAymcTrr7+O22+/HRdccAEaGxvxzW9+c9DlZs6cCUEQ0j/dJ/hEREREVLm+8Y1v4KWXXsK9996Lt956C+eccw5OPfVUrF69GgCwaNEiTJgwAX//+9+xzz77YPz48bj88stzzqxlsJaIiIiIRiTP83D11VfjX/7lX7Bx48Yhr891XWzYsKEAW0ZERERElWTDhg2YP38+HnjgAcyYMQMTJ07Ed77zHRx33HGYP38+AODjjz/G+vXr8cADD+DOO+/EggULsGLFCnzpS1/KaSzWrCUiIiKiEenKK6/E7bffnvF3Y8eOxYknnogpU6agvr4ePp8PnZ2d2L17N1avXo0VK1bgzTffRCKRKPFWExEREVGpvf3223AcB5MmTeo1PZFIoLa2FkDqwn0ikcCdd96Znu+OO+7AtGnT8MEHH2RdGoHBWiIiIiIacR5++OGMgdrDDjsMN910E0488UQIgjDgOqLRKBYvXoyHHnoIDz30ELq6uoq1uURERERURp2dnZAkCStWrIAkSb1+5/f7AQDNzc2QZblXQPeAAw4AkMrMZbCWiIiIiCgDz/PwrW99q8/0s846C3fffTc0TctqPaZp4qyzzsJZZ52FtrY2zJ8/H59++mmhN5eIiIiIyuzQQw+F4zjYtm0bZsyYkXGeY489FrZtY82aNZg4cSIA4MMPPwQAjBs3LuuxGKwlIiIiohHlxRdfxLp163pNGzVqFBYsWJB1oHZvoVAI//Zv/zb0jSMiIiKisujs7MRHH32Ufr527Vq88cYbiEQimDRpEi644AJcfPHF+PWvf41DDz0U27dvx1NPPYWpU6fi9NNPx0knnYTDDjsMl112GX7729/CdV18/etfx8knn9ynfMJA2GCMiIiIiEaUJ554os+0Sy+9FIFAoAxbQ0RERESVYPny5Tj00ENx6KGHAgC+/e1v49BDD8WPf/xjAMD8+fNx8cUX49///d+x//7748wzz8Rrr72GsWPHAgBEUcSiRYtQV1eH448/HqeffjoOOOAA3HvvvTltBzNriYiIiGhEWb9+fZ9p06ZNK8OWFJ/jOFi5ciXWr1+P7du3Y/fu3QgGg6ivr8d+++2HQw89dNDavEPV0dGBl19+GatXr0ZraytM00RLSwumTJmCKVOmFHSsTz75BG+++SY+/fRTtLe3I5FIwDAM+Hw+tLS0YPz48Zg0aRJ0XS/ouERERFT9Zs6cCc/z+v29oij46U9/ip/+9Kf9ztPS0oKFCxcOaTsYrCUiIiKiEWXbtm19pvl8vqKOOVBA9LnnnssqYLp27VqMHz8+q/GWLl2K2267DUuXLkVra2u/89XW1uL000/HD37wA0yePDmrdXdbsGAB5s2b1+82vvHGG7jxxhuxaNEiWJaVcR2TJ0/GV7/6VVx77bVQFCWn8btt2rQJt9xyC+65554+5S0yUVUVhxxyCE466SScc845OOSQQ/Ial4iIiKgYWAaBiIiIiEaUTFmVmbJtq9Hbb7+N2bNn4+STT8aDDz44YKAWAHbu3Ik777wTBx54IK688kokEomCbMfPf/5zHHHEEVi4cGG/gVoAWLVqFb773e/isMMOw+uvv57zOL///e+x//774z//8z+zCtQCgGVZePXVV/GLX/wChx56KOLxeM7jEhERERULg7VERERENKI0NTX1mXb//feXYUsKa9GiRTjmmGPw9NNP57ys4zi47bbbMHPmzIyZx7m47rrr8KMf/Qi2bWe9zDvvvINZs2Zh+fLlWS9z/fXX4xvf+AY6Ozvz2UwiIiKiisQyCEREREQ0okyfPh233357r2lLly7F7373O1xzzTVFGfPggw9OP/7oo4/Q1dWVfu7z+bDvvvsOug5VVfv93d13342LLroIruv2WebEE0/EUUcdhTFjxiAUCqGzsxPr1q3DU089hWXLlvWa/+WXX8ZZZ52FZ555Jq+yBA888ABuuumm9HNd13HaaadhxowZaG5uRmdnJ9asWYOHHnoIH3zwQa9l29racPLJJ2PFihWYMGHCgOM8++yz+PnPf95neigUwsknn4xDDjkEY8aMgc/nQyKRQEdHBzZs2IB3330Xr7zyCrZs2ZLz/42IiIioFARvoMq5RERERETDzPbt2zFu3DjEYrE+v5s7dy6+973v4bjjjiva+DNnzsRzzz2Xfn7CCSfg2WefzXt97777Lo488khEo9H0NFmW8a1vfQvf/e53UV9f3++yb7zxBi6//HKsWLGi1/TvfOc7+NWvfjXguJlq1uq6ni4rcPrpp+P2229HS0tLxuXvvPNOXHvttX1KNZx44olYunTpgHV8TzrpJDz11FO9pv37v/87brjhBgQCgQG32/M8rFy5Evfddx/uuOMOfPrpp2w4RkRERBWDwVoiIiIiGnG+973vDRiMHDVqFD7/+c/jmGOOwZFHHokpU6ZAlgtzU1ohg7Wu6+Lggw/GO++8k57m8/mwaNEizJo1K6t1WJaFM844A0uWLElPU1UVa9aswejRo/tdLlOwttv555+Pu+66C6I4cNW11157DbNnz0ZHR0ev6XfeeScuuuiijMu0t7cjEonAcZz0tEsvvRTz588fcKxMYrEYdF3PqsEbERERUSmwZi0RERERjTg/+9nPcMwxx/T7+08//RTz58/HFVdcgUMOOQSBQABHHXUUvvnNb+LBBx/E1q1bS7i1/Vu4cGGvQC0AzJ8/P+tALZAKzD7wwAOoq6tLT7MsC7/5zW/y2qZJkyZh/vz5gwZqAeCII47A7373uz7T//u//7vfZdavX98rUAsA//qv/5r7hgIwDIOBWiIiIqooDNYSERER0Yij6zoef/xxzJ07N6v54/E4Xn31Vfzud7/DOeecg+bmZsycORN/+tOfkEgkiry1/fvlL3/Z6/nMmTNxzjnn5LyeUCiEa6+9tte0hx56KK9t+vWvfz1gfd29XXzxxTj88MN7TVuxYkW/zcb2zsIFgNra2tw2koiIiKhCMVhLRERERCNSOBzGI488gjvvvDOrBl89eZ6H5557Dl/96lcxadIk/PWvfy3SVvZv3bp1fWrNXn755Xmv7/TTT++z/vXr1+e0jlGjRmHOnDk5LSMIQsbM2CeeeCLj/JkCs3s3SiMiIiKqVgzWEhEREdGIJQgCLrroIrz//vtYtGgRzjvvPASDwZzWsWHDBlx44YWYN29eSbNse9a97Xbsscfmvb599tmnz7TXX389p3XMnTs3q/IHezvrrLP6THv55ZczzrvffvshEon0mnbdddfh+eefz3lcIiIiokpTmC4JRERERERVTJZlnHHGGTjjjDPgOA7eeOMNLFu2DK+99hpef/11fPDBB33qpO5twYIFiEajuO+++0qyzS+88EKfaWeeeWZBx9ixY0dO80+bNi2vcerq6jBmzBh88skn6Wl7Zw13E0URF198MX7729+mp+3cuRPHH388Tj75ZFx44YWYM2dOrxq8RERERNWCwVoiIiIioh4kScK0adN6BR6j0SheeeUVPPPMM3jggQewatWqjMvef//9OO6443DNNdcUfTs3btzYZ9qbb75Z0DF27tyZ0/z7779/3mNNnjy5V7B2x44d8DwvYwOwH/3oR3j00Ufx8ccf95q+ZMkSLFmyBIIgYMqUKZg+fTqOOOIIzJgxY0jbRkRERFQqLINARERERDQI0zQxa9Ys/OxnP8P777+PxYsXY8qUKRnnvfHGGxGNRou+TbkGUvMRi8Vymj8UCuU91t7LOo6TsZkYkKpbu3TpUhx66KEZf+95Ht555x3cfvvt+Nd//VdMnjwZzc3NuPLKKzOWjyAiIiKqFAzWEhERERHl6JRTTsFrr72G0047rc/vtm3bhkcffbTo27B79+6ij5Ern89X0GX7C9YCqRq7r7zyCm699dassma3bNmC2267DTNnzsQRRxyBZ555Ju9tJSIiIioWBmuJiIiIiPJgGAbuvffejLVRn3rqqZKMv7dYLAbP8wr285Of/CSnberq6sr7/5Np2UAgMOAyiqLga1/7GlatWoXly5fjxhtvxOc///lBm8QtX74cs2fPxs9//vO8t5eIiIioGBisJSIiIiLKUzAYxKWXXtpn+gcffFD0sTMFiXft2lX0cQfS1tZWsGUlSRo0WNvTtGnT8MMf/hD/+Mc/sHv3brz11lv4/e9/jy996UsZg7ee5+FHP/oR/vrXv+a9zURERESFxmAtEREREdEQHHnkkX2m7dixo+jjNjY29pm2fv36oo87kA8//DDvZfcOcNfV1WVsLpYNURRx0EEH4eqrr8YDDzyAbdu24e6778akSZP6zHvdddfBtu28xiEiIiIqNAZriYiIiIiGIFNTLVmWiz7uUUcd1WfaP//5z6KPO5AVK1bktdyOHTuwYcOGXtOmTZtWiE0CAGiahvPOOw8rVqzo05Ts008/xcsvv1ywsYiIiIiGgsFaIiIiIqIh2Lp1a59pmbJeu+0dyHUcJ69xTz755D7T/va3v+W1rkJ59NFH4bpuzstl2u6jjz66EJvUi9/vxw033NBn+ltvvVXwsYiIiIjywWAtEREREdEQPP30032mTZw4sd/5967D2tnZmde4n/vc57Dffvv1mvbqq69m3J5S+fTTT/HEE0/kvNwdd9zRZ9ppp51WiE3qY/LkyX2mDaXWLhEREVEhMVhLRERERCPKokWLsHbt2oKsa82aNbj//vv7TD/99NP7XaampqbX87Vr18LzvLzG/+EPf9hn2uWXX17WRmPf+c53kEwms57/rrvuwquvvtpr2rRp03D44YcXetMAAJs3b+4zrb6+vihjEREREeWKwVoiIiIiGlEee+wxTJo0CfPmzcOqVavyXs+mTZvwxS9+EdFotNf0+vr6jCUKuh100EG9nre1teHFF1/MaxsuvPDCPpmia9euxZw5c7Bp06a81tne3o6bbroJf/nLX/JaftWqVbj88suzCkCvXLkS3/jGN/pM/+Y3v9nvMo8++ijmz5+PRCKR1/b993//d59pBx98cF7rIiIiIio0BmuJiIiIaMSxbRsLFizAAQccgKOPPhq33HJLxozLTKLRKG699VYceuihePvtt/v8/le/+hV0Xe93+enTp/eZdtlll+GZZ57Jud6rJEl44IEH+pRWeOWVV3DooYfif//3fxGPxwddj23bWLp0Ka644gqMHTsW1113HbZs2ZLTtgBI/7/vvPNOnHnmmQP+Tf/617/ipJNOQnt7e6/ps2bNwkUXXdTvch9//DEuu+wyjB07Ft/+9rexbNmyrP5uO3bswCWXXIKHH3641/RJkybhiCOOGHR5IiIiolIQvHzvuSIiIiIiqkJXXnklbrvttoy/Gz9+PI466ih87nOfQ11dHWprayEIAtrb27F+/Xq8+eabePrpp9HV1ZVx+XPPPRf33XffoNswZcoUvPfee32mG4aB0aNHwzTNPr97/PHH0dLSknF9ixYtwllnnQXbtvv8LhwO44QTTsBRRx2FhoYGhEIhdHV1obW1FRs2bMCKFSvw+uuv9wma/upXv8J3vvOdfv8PCxYswLx583pNu+mmm/C9732v1/9nzpw5OO6449Dc3Iyuri589NFHeOihhzJmNYfDYaxYsQITJkzod9zf/va3+Na3vtVrWm1tLQ477DAccsghGDt2LMLhMAzDQDQaxfr16/Haa69hyZIliMVivZYTBAH/+Mc/BsyEJiIiIiolefBZiIiIiIhGhnXr1mHdunV5LXvJJZdkbJSVye9+9zt8/vOfh+M4vabHYjGsXr064zKWZfW7vrlz5+Kpp57Cl7/85T4Zsa2trXjkkUfwyCOPZLVtQ3HOOedg+/bt+NWvfgUg9f9ZuHAhFi5cOOiyoVAITz755ICB2v7s3LkTS5YswZIlS7JeRhAE/OY3v2GgloiIiCoKyyAQERER0Yhy0UUX4YILLkA4HC7I+iZMmIBHHnkECxYsgCRJWS1z4okn4qGHHkJjY2NBtgEAjj/+eKxcuRIXXnhh1tuRiSAImDVrFmbMmJHX8jfddBN++tOf5rQNU6ZMwdNPP51VOYLa2lrI8tByTsaNG4dHHnkE//Zv/zak9RAREREVGssgEBEREdGIlEwm8fzzz+O5557DsmXL8Oqrr6KzszOrZRsbG3HSSSfh/PPPx+c///m8g4fxeByLFi3CP/7xD7z99tvYsGEDOjo6EI1G+zToWrt2LcaPH5/VeteuXYv/+Z//wT/+8Q+8//77g84fCARwwgkn4KSTTsKZZ56JcePGDbpMpjIIPbdxxYoV+I//+A88/vjjSCaTGdex//7746tf/Sr+7d/+DYqiDP4f26O1tRWLFy/G0qVL8eKLL2LVqlWDNjQTRRHHHXcczj//fFxyySUD1hUmIiIiKhcGa4mIiIiIAHieh08//RSrV6/Ghg0b0N7ejo6ODgiCgGAwiEAggObmZkydOrWgGbHFtnXrVqxcuRI7duzAzp070dnZCZ/Ph2AwiFGjRmHy5MkYN24cBEHIab2DBWu7tbe34+WXX8aHH36I9vZ2GIaBlpYWTJkyBQceeOBQ/3sAgLa2NqxevRoff/wxtm3bhs7OTjiOg0AggHA4jEmTJuGggw6Cz+cryHhERERExcJgLRERERER5SzbYC0RERERZY81a4mIiIiIiIiIiIgqAIO1RERERERERERERBWAwVoiIiIiIiIiIiKiCsBgLREREREREREREVEFYLCWiIiIiIiIiIiIqAIwWEtERERERERERERUARisJSIiIiIiIiIiIqoADNYSERERERERERERVQDB8zyv3BtBRERERERERERENNIxs5aIiIiIiIiIiIioAjBYS0RERERERERERFQBGKwlIiIiIiIiIiIiqgAM1hIRERERERERERFVAAZriYiIiIiIiIiIiCoAg7VEREREREREREREFYDBWiIiIiIiIiIiIqIKwGAtERERERERERERUQVgsJaIiIiIiIiIiIioAvx/DaqxB7PDcoAAAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "df = {}\n", + "logs = [file for file in os.listdir(\"parse_results\") if \".json\" in file]\n", + "for log in logs:\n", + " print(log)\n", + " with open(f\"parse_results/{log}\", \"r\") as f:\n", + " returns = json.load(f)\n", + "\n", + " agent_1, agent_2 = list(returns.keys())\n", + " if agent_1 not in df.keys():\n", + " df[agent_1] = returns[agent_1]\n", + " if agent_2 not in df.keys():\n", + " df[agent_2] = returns[agent_2]\n", + " else:\n", + " for checkpoint in list(returns[agent_1].keys()):\n", + " df[agent_1][checkpoint].extend(returns[agent_1][checkpoint])\n", + " for checkpoint in list(returns[agent_2].keys()):\n", + " df[agent_2][checkpoint].extend(returns[agent_2][checkpoint])\n", + "\n", + "fig = plot_results(sort_and_trim_dicts(df))" + ] + }, + { + "cell_type": "code", + "execution_count": 131, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "['DR_FSP_DR_PFSP', 'DR_SP_DR_FSP', 'DR_SP_DR_PFSP']" + ] + }, + "execution_count": 131, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "def check_dir_name(dir: str):\n", + " split = dir.split(\"_\")\n", + " valid_names = [\"DR\", \"PLR\", \"SP\", \"FSP\", \"PFSP\"]\n", + " return all([x in valid_names for x in split])\n", + "\n", + "\n", + "valid_dirs = [dir for dir in os.listdir() if check_dir_name(dir)]\n", + "valid_dirs" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.7" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/syllabus/examples/experimental/round_robin_test.ipynb b/syllabus/examples/experimental/round_robin_test.ipynb new file mode 100644 index 00000000..b45a6d76 --- /dev/null +++ b/syllabus/examples/experimental/round_robin_test.ipynb @@ -0,0 +1,1033 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import pandas as pd\n", + "import plotly.express as px\n", + "import plotly.graph_objects as go\n", + "import seaborn as sns\n", + "import json\n", + "import numpy as np" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "with open(\"round_robin/DR_FSP_DR_SP.json\") as f:\n", + " logs = json.load(f)" + ] + }, + { + "cell_type": "code", + "execution_count": 81, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.plotly.v1+json": { + "config": { + "plotlyServerURL": "https://plotly.com" + }, + "data": [ + { + "line": { + "color": "blue" + }, + "mode": "lines", + "name": "Average", + "type": "scatter", + "x": [ + 0, + 2000, + 4000, + 6000, + 8000 + ], + "y": [ + -0.010416666666666664, + -0.027777777777777776, + -0.010416666666666666, + -0.03472222222222222, + -0.003472222222222219 + ] + }, + { + "fill": "toself", + "fillcolor": "rgba(0, 0, 255, 0.2)", + "line": { + "color": "rgba(255, 255, 255, 0)" + }, + "name": "Error Band", + "type": "scatter", + "x": [ + 0, + 2000, + 4000, + 6000, + 8000, + 8000, + 6000, + 4000, + 2000, + 0 + ], + "y": [ + 0.18473952078328326, + 0.17444749692445882, + 0.2550891617202209, + 0.19840847682770163, + 0.22471909445344107, + -0.2316635388978855, + -0.26785292127214605, + -0.27592249505355426, + -0.2300030524800144, + -0.20557285411661658 + ] + } + ], + "layout": { + "showlegend": true, + "template": { + "data": { + "bar": [ + { + "error_x": { + "color": "#2a3f5f" + }, + "error_y": { + "color": "#2a3f5f" + }, + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "bar" + } + ], + "barpolar": [ + { + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "barpolar" + } + ], + "carpet": [ + { + "aaxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "baxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "type": "carpet" + } + ], + "choropleth": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "choropleth" + } + ], + "contour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "contour" + } + ], + "contourcarpet": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "contourcarpet" + } + ], + "heatmap": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "heatmap" + } + ], + "heatmapgl": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "heatmapgl" + } + ], + "histogram": [ + { + "marker": { + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "histogram" + } + ], + "histogram2d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "histogram2d" + } + ], + "histogram2dcontour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "histogram2dcontour" + } + ], + "mesh3d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "mesh3d" + } + ], + "parcoords": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "parcoords" + } + ], + "pie": [ + { + "automargin": true, + "type": "pie" + } + ], + "scatter": [ + { + "fillpattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + }, + "type": "scatter" + } + ], + "scatter3d": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatter3d" + } + ], + "scattercarpet": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattercarpet" + } + ], + "scattergeo": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergeo" + } + ], + "scattergl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergl" + } + ], + "scattermapbox": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattermapbox" + } + ], + "scatterpolar": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolar" + } + ], + "scatterpolargl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolargl" + } + ], + "scatterternary": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterternary" + } + ], + "surface": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "surface" + } + ], + "table": [ + { + "cells": { + "fill": { + "color": "#EBF0F8" + }, + "line": { + "color": "white" + } + }, + "header": { + "fill": { + "color": "#C8D4E3" + }, + "line": { + "color": "white" + } + }, + "type": "table" + } + ] + }, + "layout": { + "annotationdefaults": { + "arrowcolor": "#2a3f5f", + "arrowhead": 0, + "arrowwidth": 1 + }, + "autotypenumbers": "strict", + "coloraxis": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "colorscale": { + "diverging": [ + [ + 0, + "#8e0152" + ], + [ + 0.1, + "#c51b7d" + ], + [ + 0.2, + "#de77ae" + ], + [ + 0.3, + "#f1b6da" + ], + [ + 0.4, + "#fde0ef" + ], + [ + 0.5, + "#f7f7f7" + ], + [ + 0.6, + "#e6f5d0" + ], + [ + 0.7, + "#b8e186" + ], + [ + 0.8, + "#7fbc41" + ], + [ + 0.9, + "#4d9221" + ], + [ + 1, + "#276419" + ] + ], + "sequential": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "sequentialminus": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ] + }, + "colorway": [ + "#636efa", + "#EF553B", + "#00cc96", + "#ab63fa", + "#FFA15A", + "#19d3f3", + "#FF6692", + "#B6E880", + "#FF97FF", + "#FECB52" + ], + "font": { + "color": "#2a3f5f" + }, + "geo": { + "bgcolor": "white", + "lakecolor": "white", + "landcolor": "#E5ECF6", + "showlakes": true, + "showland": true, + "subunitcolor": "white" + }, + "hoverlabel": { + "align": "left" + }, + "hovermode": "closest", + "mapbox": { + "style": "light" + }, + "paper_bgcolor": "white", + "plot_bgcolor": "#E5ECF6", + "polar": { + "angularaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "radialaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "scene": { + "xaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "yaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "zaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + } + }, + "shapedefaults": { + "line": { + "color": "#2a3f5f" + } + }, + "ternary": { + "aaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "baxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "caxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "title": { + "x": 0.05 + }, + "xaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + }, + "yaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + } + } + }, + "title": { + "text": "DR_FSP" + }, + "xaxis": { + "title": { + "text": "Checkpoints" + } + }, + "yaxis": { + "title": { + "text": "Returns" + } + } + } + } + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "def plot_performances(agent_name: str, experiment: str):\n", + " with open(f\"round_robin/{experiment}.json\") as f:\n", + " logs = json.load(f)\n", + " df = {\n", + " int(key): {\n", + " \"avg\": np.array(logs[agent_name][key]).mean(),\n", + " \"std\": np.array(logs[agent_name][key]).std(),\n", + " }\n", + " for key in sorted(\n", + " logs[agent_name].keys(),\n", + " )\n", + " }\n", + " df = pd.DataFrame(df.values(), index=df.keys(), columns=[\"avg\", \"std\"])\n", + " df[\"lower\"] = df[\"avg\"] - df[\"std\"]\n", + " df[\"upper\"] = df[\"avg\"] + df[\"std\"]\n", + "\n", + " fig = go.Figure()\n", + "\n", + " fig.add_trace(\n", + " go.Scatter(\n", + " x=df.index,\n", + " y=df[\"avg\"],\n", + " mode=\"lines\",\n", + " line=dict(color=\"blue\"),\n", + " name=\"Average\",\n", + " )\n", + " )\n", + "\n", + " fig.add_trace(\n", + " go.Scatter(\n", + " x=np.array([df.index, df.index[::-1]]).flatten(),\n", + " y=np.array([df[\"upper\"], df[\"lower\"][::-1]]).flatten(),\n", + " fill=\"toself\",\n", + " fillcolor=\"rgba(0, 0, 255, 0.2)\",\n", + " line=dict(color=\"rgba(255, 255, 255, 0)\"),\n", + " name=\"Error Band\",\n", + " )\n", + " )\n", + "\n", + " fig.update_layout(\n", + " title=agent_name,\n", + " xaxis_title=\"Checkpoints\",\n", + " yaxis_title=\"Returns\",\n", + " showlegend=True,\n", + " )\n", + " fig.show()\n", + "\n", + "\n", + "plot_performances(\"DR_FSP\", \"DR_FSP_DR_SP\")" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "15000" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "n = 3 # n_curriculums \n", + "C = 40 # n_checkpoints \n", + "S = 5 # n_seeds\n", + "E = 5 # n_episodes_per_matchup \n", + "total_episodes = n*C*S**2*E\n", + "total_episodes" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.7" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/syllabus/examples/experimental/run_lasertag_experiments.sh b/syllabus/examples/experimental/run_lasertag_experiments.sh new file mode 100644 index 00000000..c353c32f --- /dev/null +++ b/syllabus/examples/experimental/run_lasertag_experiments.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +declare -a curricula=("SP" "FSP" "PFSP") + +for curriculum in "${curricula[@]}"; do + + for seed in {0..10}; do + sbatch < Config:\n", + " init(autoreset=True) # Initialize colorama\n", + " config = tyro.cli(Config)\n", + " config.batch_size = int(config.num_workers * config.rollout_length)\n", + " print(\n", + " f\"{Fore.RED}{Style.BRIGHT} Setting BATCH_SIZE to: NUM_WORKERS * ROLLOUT_LENGTH\"\n", + " f\"= {config.batch_size}\"\n", + " )\n", + " config.minibatch_size = int(config.batch_size // config.num_minibatches)\n", + " print(\n", + " f\"{Fore.RED}{Style.BRIGHT} Setting MINIBATCH_SIZE to:\"\n", + " f\"BATCH_SIZE * NUM_MINIBATCHES= {config.minibatch_size}\"\n", + " )\n", + " log_config(config)\n", + " print(\n", + " f\"{Fore.BLUE}{Style.BRIGHT} RUNNING PPO ON {config.env_id} \"\n", + " f\"FOR {config.total_timesteps} TIMESTEPS ...\"\n", + " )\n", + " return config\n", + "\n", + "\n", + "def log_config(config: Config) -> None:\n", + " config_dict = config.__dict__\n", + " print(f\"{Fore.GREEN}{Style.BRIGHT} Config:\")\n", + " log_str = \" \\n\".join(\n", + " [\n", + " f\"{key.replace('_', ' ').capitalize()}: {Fore.GREEN}{value}{Style.RESET_ALL}\"\n", + " for key, value in config_dict.items()\n", + " ]\n", + " )\n", + " print(log_str)\n", + "\n", + "\n", + "def log_rewards(r_batch: torch.tensor, run, step: int) -> None:\n", + " r_logs = {f\"agent_{agent_idx}\": reward for agent_idx, reward in enumerate(r_batch)}\n", + "\n", + " run[\"charts/cumulative_rewards_per_update\"].append(r_logs, step=step)\n", + "\n", + "\n", + "def batchify(x, device=None):\n", + " \"\"\"Converts PZ style returns to batch of torch arrays.\"\"\"\n", + " # convert to list of np arrays\n", + " x = np.stack([x[a] for a in x], axis=0)\n", + " # convert to torch\n", + " x = torch.tensor(x)\n", + "\n", + " if device is not None:\n", + " x = x.to(device)\n", + "\n", + " return x\n", + "\n", + "\n", + "def unbatchify(x, possible_agents: np.ndarray):\n", + " \"\"\"Converts np array to PZ style arguments.\"\"\"\n", + " x = x.cpu().numpy()\n", + " x = {agent: x[idx] for idx, agent in enumerate(possible_agents)}\n", + "\n", + " return x\n", + "\n", + "\n", + "class LasertagParallelWrapper(PettingZooTaskWrapper):\n", + " \"\"\"\n", + " Wrapper ensuring compatibility with the PettingZoo Parallel API.\n", + "\n", + " Lasertag Environment:\n", + " * Action shape: `n_agents` * `Discrete(5)`\n", + " * Observation shape: Dict('image': Box(0, 255, (`n_agents`, 3, 5, 5), uint8))\n", + " \"\"\"\n", + "\n", + " def __init__(self, n_agents, *args, **kwargs):\n", + " super().__init__(*args, **kwargs)\n", + " self.n_agents = n_agents\n", + " self.task = None\n", + " self.episode_return = 0\n", + " self.possible_agents = [f\"agent_{i}\" for i in range(self.n_agents)]\n", + " self.env.agents = self.possible_agents\n", + " self.n_steps = 0\n", + " self.env.render_mode = \"human\"\n", + "\n", + " def observation_space(self, agent):\n", + " env_space = self.env.observation_space[\"image\"]\n", + " # Remove agent dimension\n", + " return gymnasium.spaces.Box(\n", + " low=env_space.low[0],\n", + " high=env_space.high[0],\n", + " shape=env_space.shape[1:],\n", + " dtype=env_space.dtype,\n", + " )\n", + "\n", + " def action_space(self, agent):\n", + " return gymnasium.spaces.Discrete(5)\n", + "\n", + " def __getattr__(self, name):\n", + " \"\"\"\n", + " Delegate attribute lookup to the wrapped environment if the attribute\n", + " is not found in the LasertagParallelWrapper instance.\n", + " \"\"\"\n", + " return getattr(self.env, name)\n", + "\n", + " def _np_array_to_pz_dict(self, array: np.ndarray) -> Dict[str, np.ndarray]:\n", + " \"\"\"\n", + " Returns a dictionary containing individual observations for each agent.\n", + " Assumes that the batch dimension represents individual agents.\n", + " \"\"\"\n", + " out = {}\n", + " for idx, value in enumerate(array):\n", + " out[self.possible_agents[idx]] = value\n", + " return out\n", + "\n", + " def _singleton_to_pz_dict(self, value: bool) -> Dict[str, bool]:\n", + " \"\"\"\n", + " Broadcasts the `done` and `trunc` flags to dictionaries keyed by agent id.\n", + " \"\"\"\n", + " return {agent: value for agent in self.agents}\n", + "\n", + " def reset(\n", + " self, seed: int = None\n", + " ) -> Tuple[Dict[AgentID, ObsType], Dict[AgentID, dict]]:\n", + " \"\"\"\n", + " Resets the environment and returns a dictionary of observations\n", + " keyed by agent ID.\n", + " \"\"\"\n", + " self.env.seed(seed)\n", + " obs = self.env.reset_random() # random level generation\n", + " pz_obs = self._np_array_to_pz_dict(obs[\"image\"])\n", + " return pz_obs, {}\n", + "\n", + " def step(\n", + " self,\n", + " action: Dict[AgentID, ActionType],\n", + " ) -> Tuple[\n", + " Dict[AgentID, ObsType],\n", + " Dict[AgentID, float],\n", + " Dict[AgentID, bool],\n", + " Dict[AgentID, bool],\n", + " Dict[AgentID, dict],\n", + " ]:\n", + " \"\"\"\n", + " Takes inputs in the PettingZoo (PZ) Parallel API format, performs a step and\n", + " returns outputs in PZ format.\n", + " \"\"\"\n", + " action = batchify(action)\n", + " obs, rew, done, info = self.env.step(action)\n", + " obs = obs[\"image\"]\n", + " trunc = False # there is no `truncated` flag in this environment\n", + " self.task_completion = self._task_completion(obs, rew, done, trunc, info)\n", + " # convert outputs back to PZ format\n", + " obs, rew = map(self._np_array_to_pz_dict, [obs, rew])\n", + " done, trunc, info = map(\n", + " self._singleton_to_pz_dict, [done, trunc, self.task_completion]\n", + " )\n", + " # info[\"agent_id\"] = agent_task\n", + " self.n_steps += 1\n", + " return self.observation(obs), rew, done, trunc, info" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def make_env():\n", + " env = LasertagAdversarial()\n", + " env = LasertagParallelWrapper(env=env, n_agents=2)\n", + " env = PettingZooPufferEnv(env)\n", + " return env" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class Agent(nn.Module):\n", + " def __init__(self, envs) -> None:\n", + " super().__init__()\n", + "\n", + " self.conv = nn.Sequential(\n", + " self.layer_init(\n", + " nn.Conv2d(\n", + " envs.single_observation_space.shape[0],\n", + " out_channels=16,\n", + " kernel_size=3,\n", + " stride=1,\n", + " )\n", + " ),\n", + " nn.Flatten(),\n", + " nn.ReLU(),\n", + " )\n", + " self.lstm = nn.LSTM(input_size=16 * 3 * 3, hidden_size=256, batch_first=True)\n", + " self.lstm_init()\n", + "\n", + " self.mlp = nn.Sequential(\n", + " nn.ReLU(),\n", + " self.layer_init(nn.Linear(256, 32)),\n", + " nn.ReLU(),\n", + " )\n", + " self.actor = self.layer_init(\n", + " nn.Linear(32, envs.single_action_space.n), scale=0.01\n", + " )\n", + " self.critic = self.layer_init(nn.Linear(32, 1), scale=1)\n", + "\n", + " def get_states(self, x, lstm_states, done):\n", + " # x shape: (num_envs, *obs_shape)\n", + " batch_size = x.size(0)\n", + " hidden = self.conv(x / 255.0)\n", + " hidden = hidden.view(batch_size, 1, -1) # shape: (num_envs, 1, n_features)\n", + "\n", + " # Reshape LSTM states\n", + " h_states, c_states = lstm_states\n", + " h_states = h_states.view(1, batch_size, -1)\n", + " c_states = c_states.view(1, batch_size, -1)\n", + "\n", + " # Apply the LSTM\n", + " hidden, (new_h_states, new_c_states) = self.lstm(hidden, (h_states, c_states))\n", + "\n", + " # Reset LSTM state if done\n", + " done = done.view(batch_size, 1, 1)\n", + " new_h_states = new_h_states * (1 - done)\n", + " new_c_states = new_c_states * (1 - done)\n", + "\n", + " hidden = hidden.view(batch_size, -1)\n", + " new_lstm_states = (\n", + " new_h_states.view(batch_size, -1),\n", + " new_c_states.view(batch_size, -1),\n", + " )\n", + "\n", + " return hidden, new_lstm_states\n", + "\n", + " def get_value(self, x, lstm_states, done):\n", + " hidden, _ = self.get_states(x, lstm_states, done)\n", + " hidden = self.mlp(hidden)\n", + " return self.critic(hidden)\n", + "\n", + " def get_action_and_value(self, x, lstm_states, done, action=None):\n", + " hidden, new_lstm_states = self.get_states(x, lstm_states, done)\n", + " hidden = self.mlp(hidden)\n", + " logits = self.actor(hidden)\n", + " probs = Categorical(logits=logits)\n", + "\n", + " if action is None:\n", + " action = probs.sample()\n", + "\n", + " return (\n", + " action,\n", + " probs.log_prob(action),\n", + " probs.entropy(),\n", + " self.critic(hidden),\n", + " new_lstm_states,\n", + " )\n", + "\n", + " def layer_init(self, layer, scale=np.sqrt(2), bias_const=0.0):\n", + " torch.nn.init.orthogonal_(layer.weight, scale)\n", + " torch.nn.init.constant_(layer.bias, bias_const)\n", + " return layer\n", + "\n", + " def lstm_init(self):\n", + " for name, param in self.lstm.named_parameters():\n", + " if \"bias\" in name:\n", + " nn.init.constant_(param, 0)\n", + " elif \"weight\" in name:\n", + " nn.init.orthogonal_(param, 1.0)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Discrete(5)\n", + "Box(0, 255, (3, 5, 5), uint8)\n" + ] + } + ], + "source": [ + "args = Config()\n", + "args.batch_size = int(args.num_workers * args.rollout_length)\n", + "args.minibatch_size = int(args.batch_size // args.num_minibatches)\n", + "random.seed(args.seed)\n", + "np.random.seed(args.seed)\n", + "torch.manual_seed(args.seed)\n", + "torch.backends.cudnn.deterministic = args.torch_deterministic\n", + "\n", + "device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n", + "\n", + "# env setup\n", + "envs = [make_env for _ in range(args.num_workers)]\n", + "envs = Serial(\n", + " envs,\n", + " [() for _ in range(args.num_workers)],\n", + " [{} for _ in range(args.num_workers)],\n", + " args.num_workers,\n", + ")\n", + "print(envs.single_action_space)\n", + "print(envs.single_observation_space)\n", + "envs.is_vector_env = True\n", + "\n", + "agent = Agent(envs).to(device)\n", + "optimizer = optim.Adam(agent.parameters(), lr=args.adam_lr, eps=args.adam_eps)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "obs = torch.zeros(\n", + " (args.rollout_length, args.num_workers * 2)\n", + " + envs.single_observation_space.shape\n", + ").to(device)\n", + "actions = torch.zeros(\n", + " (args.rollout_length, args.num_workers * 2) + envs.single_action_space.shape\n", + ").to(device)\n", + "logprobs = torch.zeros((args.rollout_length, args.num_workers * 2)).to(device)\n", + "rewards = torch.zeros((args.rollout_length, args.num_workers * 2)).to(device)\n", + "dones = torch.zeros((args.rollout_length, args.num_workers * 2)).to(device)\n", + "values = torch.zeros((args.rollout_length, args.num_workers * 2)).to(device)\n", + "\n", + "lstm_state = (\n", + " torch.zeros(agent.lstm.num_layers, args.num_workers, agent.lstm.hidden_size).to(device),\n", + " torch.zeros(agent.lstm.num_layers, args.num_workers, agent.lstm.hidden_size).to(device),\n", + ")\n", + "lstm_state_opponent = (\n", + " torch.zeros(agent.lstm.num_layers, args.num_workers, agent.lstm.hidden_size).to(device),\n", + " torch.zeros(agent.lstm.num_layers, args.num_workers, agent.lstm.hidden_size).to(device),\n", + ")\n", + "\n", + "# TRY NOT TO MODIFY: start the game\n", + "global_step = 0\n", + "start_time = time.time()\n", + "next_obs, info = envs.reset()\n", + "next_obs = torch.Tensor(next_obs).to(device)\n", + "next_done = torch.zeros(args.num_workers * 2).to(device)\n", + "num_updates = int(args.total_timesteps // args.batch_size)\n", + "\n", + "cumulative_rewards = np.zeros(args.num_workers * 2)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[torch.Size([16, 3, 5, 5]), torch.Size([1, 8, 256]), torch.Size([16])]" + ] + }, + "execution_count": 82, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "list(map(lambda x: x.shape, (next_obs, lstm_state[0], next_done)))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def split_batch(joint_obs: torch.Tensor) -> Tuple[torch.Tensor]:\n", + " \"\"\"Splits a batch of joint data in agent and opponent data.\"\"\"\n", + " agent_data = joint_obs[[i for i in np.arange(0, args.num_workers * 2, 2)]]\n", + " opponent_data = joint_obs[[i for i in np.arange(1, args.num_workers * 2, 2)]]\n", + " return agent_data, opponent_data" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "ename": "RuntimeError", + "evalue": "CUDA error: device-side assert triggered\nCUDA kernel errors might be asynchronously reported at some other API call, so the stacktrace below might be incorrect.\nFor debugging consider passing CUDA_LAUNCH_BLOCKING=1.\nCompile with `TORCH_USE_CUDA_DSA` to enable device-side assertions.\n", + "output_type": "error", + "traceback": [ + "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[1;31mRuntimeError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[1;32mIn[133], line 2\u001b[0m\n\u001b[0;32m 1\u001b[0m agent_obs, opponent_obs \u001b[38;5;241m=\u001b[39m split_batch(next_obs)\n\u001b[1;32m----> 2\u001b[0m agent_done, opponent_done \u001b[38;5;241m=\u001b[39m \u001b[43msplit_batch\u001b[49m\u001b[43m(\u001b[49m\u001b[43mnext_done\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 4\u001b[0m agent\u001b[38;5;241m.\u001b[39mget_action_and_value(agent_obs, lstm_state, agent_done)\n", + "Cell \u001b[1;32mIn[127], line 4\u001b[0m, in \u001b[0;36msplit_batch\u001b[1;34m(joint_obs)\u001b[0m\n\u001b[0;32m 2\u001b[0m \u001b[38;5;250m\u001b[39m\u001b[38;5;124;03m\"\"\"Splits a batch of joint data in agent and opponent data.\"\"\"\u001b[39;00m\n\u001b[0;32m 3\u001b[0m agent_data \u001b[38;5;241m=\u001b[39m joint_obs[[i \u001b[38;5;28;01mfor\u001b[39;00m i \u001b[38;5;129;01min\u001b[39;00m np\u001b[38;5;241m.\u001b[39marange(\u001b[38;5;241m0\u001b[39m, args\u001b[38;5;241m.\u001b[39mnum_workers \u001b[38;5;241m*\u001b[39m \u001b[38;5;241m2\u001b[39m, \u001b[38;5;241m2\u001b[39m)]]\n\u001b[1;32m----> 4\u001b[0m opponent_data \u001b[38;5;241m=\u001b[39m \u001b[43mjoint_obs\u001b[49m\u001b[43m[\u001b[49m\u001b[43m[\u001b[49m\u001b[43mi\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43;01mfor\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mi\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;129;43;01min\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mnp\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43marange\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m1\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43margs\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mnum_workers\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43m \u001b[49m\u001b[38;5;241;43m2\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m2\u001b[39;49m\u001b[43m)\u001b[49m\u001b[43m]\u001b[49m\u001b[43m]\u001b[49m\n\u001b[0;32m 5\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m agent_data, opponent_data\n", + "\u001b[1;31mRuntimeError\u001b[0m: CUDA error: device-side assert triggered\nCUDA kernel errors might be asynchronously reported at some other API call, so the stacktrace below might be incorrect.\nFor debugging consider passing CUDA_LAUNCH_BLOCKING=1.\nCompile with `TORCH_USE_CUDA_DSA` to enable device-side assertions.\n" + ] + } + ], + "source": [ + "agent_obs, opponent_obs = split_batch(next_obs)\n", + "agent_done, opponent_done = split_batch(next_done)\n", + "\n", + "agent.get_action_and_value(agent_obs, lstm_state, agent_done)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "assert" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + " 0%| | 0/48 [00:00 16\u001b[0m action, logprob, _, value, lstm_state \u001b[38;5;241m=\u001b[39m \u001b[43magent\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mget_action_and_value\u001b[49m\u001b[43m(\u001b[49m\n\u001b[0;32m 17\u001b[0m \u001b[43m \u001b[49m\u001b[43mnext_obs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mlstm_state\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mnext_done\u001b[49m\n\u001b[0;32m 18\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 19\u001b[0m values[step] \u001b[38;5;241m=\u001b[39m value\u001b[38;5;241m.\u001b[39mflatten()\n\u001b[0;32m 20\u001b[0m actions[step] \u001b[38;5;241m=\u001b[39m action\n", + "Cell \u001b[1;32mIn[5], line 63\u001b[0m, in \u001b[0;36mAgent.get_action_and_value\u001b[1;34m(self, x, lstm_states, done, action)\u001b[0m\n\u001b[0;32m 62\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mget_action_and_value\u001b[39m(\u001b[38;5;28mself\u001b[39m, x, lstm_states, done, action\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mNone\u001b[39;00m):\n\u001b[1;32m---> 63\u001b[0m hidden, new_lstm_states \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mget_states\u001b[49m\u001b[43m(\u001b[49m\u001b[43mx\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mlstm_states\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mdone\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 64\u001b[0m hidden \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mmlp(hidden)\n\u001b[0;32m 65\u001b[0m logits \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mactor(hidden)\n", + "Cell \u001b[1;32mIn[5], line 42\u001b[0m, in \u001b[0;36mAgent.get_states\u001b[1;34m(self, x, lstm_states, done)\u001b[0m\n\u001b[0;32m 39\u001b[0m c_states \u001b[38;5;241m=\u001b[39m c_states\u001b[38;5;241m.\u001b[39mview(\u001b[38;5;241m1\u001b[39m, batch_size, \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m1\u001b[39m)\n\u001b[0;32m 41\u001b[0m \u001b[38;5;66;03m# Apply the LSTM\u001b[39;00m\n\u001b[1;32m---> 42\u001b[0m hidden, (new_h_states, new_c_states) \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mlstm\u001b[49m\u001b[43m(\u001b[49m\u001b[43mhidden\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m(\u001b[49m\u001b[43mh_states\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mc_states\u001b[49m\u001b[43m)\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 44\u001b[0m \u001b[38;5;66;03m# Reset LSTM state if done\u001b[39;00m\n\u001b[0;32m 45\u001b[0m done \u001b[38;5;241m=\u001b[39m done\u001b[38;5;241m.\u001b[39mview(batch_size, \u001b[38;5;241m1\u001b[39m, \u001b[38;5;241m1\u001b[39m)\n", + "File \u001b[1;32mc:\\Users\\ryanp\\AppData\\Local\\Programs\\Python\\Python310\\lib\\site-packages\\torch\\nn\\modules\\module.py:1532\u001b[0m, in \u001b[0;36mModule._wrapped_call_impl\u001b[1;34m(self, *args, **kwargs)\u001b[0m\n\u001b[0;32m 1530\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_compiled_call_impl(\u001b[38;5;241m*\u001b[39margs, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs) \u001b[38;5;66;03m# type: ignore[misc]\u001b[39;00m\n\u001b[0;32m 1531\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[1;32m-> 1532\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_call_impl(\u001b[38;5;241m*\u001b[39margs, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs)\n", + "File \u001b[1;32mc:\\Users\\ryanp\\AppData\\Local\\Programs\\Python\\Python310\\lib\\site-packages\\torch\\nn\\modules\\module.py:1541\u001b[0m, in \u001b[0;36mModule._call_impl\u001b[1;34m(self, *args, **kwargs)\u001b[0m\n\u001b[0;32m 1536\u001b[0m \u001b[38;5;66;03m# If we don't have any hooks, we want to skip the rest of the logic in\u001b[39;00m\n\u001b[0;32m 1537\u001b[0m \u001b[38;5;66;03m# this function, and just call forward.\u001b[39;00m\n\u001b[0;32m 1538\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m (\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_backward_hooks \u001b[38;5;129;01mor\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_backward_pre_hooks \u001b[38;5;129;01mor\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_forward_hooks \u001b[38;5;129;01mor\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_forward_pre_hooks\n\u001b[0;32m 1539\u001b[0m \u001b[38;5;129;01mor\u001b[39;00m _global_backward_pre_hooks \u001b[38;5;129;01mor\u001b[39;00m _global_backward_hooks\n\u001b[0;32m 1540\u001b[0m \u001b[38;5;129;01mor\u001b[39;00m _global_forward_hooks \u001b[38;5;129;01mor\u001b[39;00m _global_forward_pre_hooks):\n\u001b[1;32m-> 1541\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m forward_call(\u001b[38;5;241m*\u001b[39margs, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs)\n\u001b[0;32m 1543\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[0;32m 1544\u001b[0m result \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mNone\u001b[39;00m\n", + "File \u001b[1;32mc:\\Users\\ryanp\\AppData\\Local\\Programs\\Python\\Python310\\lib\\site-packages\\torch\\nn\\modules\\rnn.py:907\u001b[0m, in \u001b[0;36mLSTM.forward\u001b[1;34m(self, input, hx)\u001b[0m\n\u001b[0;32m 904\u001b[0m hx \u001b[38;5;241m=\u001b[39m (hx[\u001b[38;5;241m0\u001b[39m]\u001b[38;5;241m.\u001b[39munsqueeze(\u001b[38;5;241m1\u001b[39m), hx[\u001b[38;5;241m1\u001b[39m]\u001b[38;5;241m.\u001b[39munsqueeze(\u001b[38;5;241m1\u001b[39m))\n\u001b[0;32m 905\u001b[0m \u001b[38;5;66;03m# Each batch of the hidden state should match the input sequence that\u001b[39;00m\n\u001b[0;32m 906\u001b[0m \u001b[38;5;66;03m# the user believes he/she is passing in.\u001b[39;00m\n\u001b[1;32m--> 907\u001b[0m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mcheck_forward_args\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;28;43minput\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mhx\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mbatch_sizes\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 908\u001b[0m hx \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mpermute_hidden(hx, sorted_indices)\n\u001b[0;32m 910\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m batch_sizes \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n", + "File \u001b[1;32mc:\\Users\\ryanp\\AppData\\Local\\Programs\\Python\\Python310\\lib\\site-packages\\torch\\nn\\modules\\rnn.py:822\u001b[0m, in \u001b[0;36mLSTM.check_forward_args\u001b[1;34m(self, input, hidden, batch_sizes)\u001b[0m\n\u001b[0;32m 816\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mcheck_forward_args\u001b[39m(\u001b[38;5;28mself\u001b[39m, \u001b[38;5;66;03m# type: ignore[override]\u001b[39;00m\n\u001b[0;32m 817\u001b[0m \u001b[38;5;28minput\u001b[39m: Tensor,\n\u001b[0;32m 818\u001b[0m hidden: Tuple[Tensor, Tensor],\n\u001b[0;32m 819\u001b[0m batch_sizes: Optional[Tensor],\n\u001b[0;32m 820\u001b[0m ):\n\u001b[0;32m 821\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mcheck_input(\u001b[38;5;28minput\u001b[39m, batch_sizes)\n\u001b[1;32m--> 822\u001b[0m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mcheck_hidden_size\u001b[49m\u001b[43m(\u001b[49m\u001b[43mhidden\u001b[49m\u001b[43m[\u001b[49m\u001b[38;5;241;43m0\u001b[39;49m\u001b[43m]\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mget_expected_hidden_size\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;28;43minput\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mbatch_sizes\u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 823\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43mExpected hidden[0] size \u001b[39;49m\u001b[38;5;132;43;01m{}\u001b[39;49;00m\u001b[38;5;124;43m, got \u001b[39;49m\u001b[38;5;132;43;01m{}\u001b[39;49;00m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m)\u001b[49m\n\u001b[0;32m 824\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mcheck_hidden_size(hidden[\u001b[38;5;241m1\u001b[39m], \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mget_expected_cell_size(\u001b[38;5;28minput\u001b[39m, batch_sizes),\n\u001b[0;32m 825\u001b[0m \u001b[38;5;124m'\u001b[39m\u001b[38;5;124mExpected hidden[1] size \u001b[39m\u001b[38;5;132;01m{}\u001b[39;00m\u001b[38;5;124m, got \u001b[39m\u001b[38;5;132;01m{}\u001b[39;00m\u001b[38;5;124m'\u001b[39m)\n", + "File \u001b[1;32mc:\\Users\\ryanp\\AppData\\Local\\Programs\\Python\\Python310\\lib\\site-packages\\torch\\nn\\modules\\rnn.py:260\u001b[0m, in \u001b[0;36mRNNBase.check_hidden_size\u001b[1;34m(self, hx, expected_hidden_size, msg)\u001b[0m\n\u001b[0;32m 257\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mcheck_hidden_size\u001b[39m(\u001b[38;5;28mself\u001b[39m, hx: Tensor, expected_hidden_size: Tuple[\u001b[38;5;28mint\u001b[39m, \u001b[38;5;28mint\u001b[39m, \u001b[38;5;28mint\u001b[39m],\n\u001b[0;32m 258\u001b[0m msg: \u001b[38;5;28mstr\u001b[39m \u001b[38;5;241m=\u001b[39m \u001b[38;5;124m'\u001b[39m\u001b[38;5;124mExpected hidden size \u001b[39m\u001b[38;5;132;01m{}\u001b[39;00m\u001b[38;5;124m, got \u001b[39m\u001b[38;5;132;01m{}\u001b[39;00m\u001b[38;5;124m'\u001b[39m) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[0;32m 259\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m hx\u001b[38;5;241m.\u001b[39msize() \u001b[38;5;241m!=\u001b[39m expected_hidden_size:\n\u001b[1;32m--> 260\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mRuntimeError\u001b[39;00m(msg\u001b[38;5;241m.\u001b[39mformat(expected_hidden_size, \u001b[38;5;28mlist\u001b[39m(hx\u001b[38;5;241m.\u001b[39msize())))\n", + "\u001b[1;31mRuntimeError\u001b[0m: Expected hidden[0] size (1, 16, 256), got [1, 16, 4096]" + ] + } + ], + "source": [ + "with tqdm(total=num_updates) as pbar:\n", + " for update in range(1, num_updates + 1):\n", + " # Annealing the rate if instructed to do so.\n", + " if args.anneal_lr:\n", + " frac = 1.0 - (update - 1.0) / num_updates\n", + " lrnow = frac * args.adam_lr\n", + " optimizer.param_groups[0][\"lr\"] = lrnow\n", + "\n", + " for step in range(0, args.rollout_length):\n", + " global_step += 1 * args.num_workers * 2\n", + " obs[step] = next_obs\n", + " dones[step] = next_done\n", + "\n", + " # ALGO LOGIC: action logic\n", + " with torch.no_grad():\n", + " action, logprob, _, value, lstm_state = agent.get_action_and_value(\n", + " next_obs, lstm_state, next_done\n", + " )\n", + " values[step] = value.flatten()\n", + " actions[step] = action\n", + " logprobs[step] = logprob\n", + "\n", + " # TRY NOT TO MODIFY: execute the game and log data.\n", + " next_obs, reward, done, trunc, info = envs.step(action.cpu().numpy())\n", + " rewards[step] = torch.tensor(reward).to(device).view(-1)\n", + " next_obs, next_done = torch.Tensor(next_obs).to(device), torch.Tensor(\n", + " done\n", + " ).to(device)\n", + "\n", + " cumulative_rewards = cumulative_rewards + reward\n", + "\n", + " with torch.no_grad():\n", + " next_value = agent.get_value(next_obs, lstm_state, next_done).reshape(1, -1)\n", + " advantages = torch.zeros_like(rewards).to(device)\n", + " lastgaelam = 0\n", + " for t in reversed(range(args.rollout_length)):\n", + " if t == args.rollout_length - 1:\n", + " nextnonterminal = 1.0 - next_done\n", + " nextvalues = next_value\n", + " else:\n", + " nextnonterminal = 1.0 - dones[t + 1]\n", + " nextvalues = values[t + 1]\n", + " delta = (\n", + " rewards[t] + args.gamma * nextvalues * nextnonterminal - values[t]\n", + " )\n", + " advantages[t] = lastgaelam = (\n", + " delta + args.gamma * args.gae_lambda * nextnonterminal * lastgaelam\n", + " )\n", + " returns = advantages + values\n", + "\n", + " # flatten the batch\n", + " b_obs = obs.reshape((-1,) + envs.single_observation_space.shape)\n", + " b_dones = dones.reshape((-1,))\n", + " b_logprobs = logprobs.reshape(-1)\n", + " b_actions = actions.reshape((-1,) + envs.single_action_space.shape)\n", + " b_advantages = advantages.reshape(-1)\n", + " b_returns = returns.reshape(-1)\n", + " b_values = values.reshape(-1)\n", + "\n", + " # Optimizing the policy and value network\n", + " b_inds = np.arange(args.batch_size)\n", + " clipfracs = []\n", + " for epoch in range(args.update_epochs):\n", + " np.random.shuffle(b_inds)\n", + " for start in range(0, args.batch_size, args.minibatch_size):\n", + " end = start + args.minibatch_size\n", + " mb_inds = b_inds[start:end]\n", + "\n", + " _, newlogprob, entropy, newvalue, lstm_state = (\n", + " agent.get_action_and_value(\n", + " b_obs[mb_inds],\n", + " lstm_state,\n", + " b_dones,\n", + " b_actions.long()[mb_inds],\n", + " )\n", + " )\n", + " logratio = newlogprob - b_logprobs[mb_inds]\n", + " ratio = logratio.exp()\n", + "\n", + " with torch.no_grad():\n", + " # calculate approx_kl http://joschu.net/blog/kl-approx.html\n", + " old_approx_kl = (-logratio).mean()\n", + " approx_kl = ((ratio - 1) - logratio).mean()\n", + " clipfracs += [\n", + " ((ratio - 1.0).abs() > args.clip_coef).float().mean().item()\n", + " ]\n", + "\n", + " mb_advantages = b_advantages[mb_inds]\n", + " if args.norm_adv:\n", + " mb_advantages = (mb_advantages - mb_advantages.mean()) / (\n", + " mb_advantages.std() + 1e-8\n", + " )\n", + "\n", + " # Policy loss\n", + " pg_loss1 = -mb_advantages * ratio\n", + " pg_loss2 = -mb_advantages * torch.clamp(\n", + " ratio, 1 - args.clip_coef, 1 + args.clip_coef\n", + " )\n", + " pg_loss = torch.max(pg_loss1, pg_loss2).mean()\n", + "\n", + " # Value loss\n", + " newvalue = newvalue.view(-1)\n", + " if args.clip_vloss:\n", + " v_loss_unclipped = (newvalue - b_returns[mb_inds]) ** 2\n", + " v_clipped = b_values[mb_inds] + torch.clamp(\n", + " newvalue - b_values[mb_inds],\n", + " -args.clip_coef,\n", + " args.clip_coef,\n", + " )\n", + " v_loss_clipped = (v_clipped - b_returns[mb_inds]) ** 2\n", + " v_loss_max = torch.max(v_loss_unclipped, v_loss_clipped)\n", + " v_loss = 0.5 * v_loss_max.mean()\n", + " else:\n", + " v_loss = 0.5 * ((newvalue - b_returns[mb_inds]) ** 2).mean()\n", + "\n", + " entropy_loss = entropy.mean()\n", + " loss = pg_loss - args.ent_coef * entropy_loss + v_loss * args.vf_coef\n", + "\n", + " optimizer.zero_grad()\n", + " loss.backward()\n", + " nn.utils.clip_grad_norm_(agent.parameters(), args.max_grad_norm)\n", + " optimizer.step()\n", + "\n", + " if args.target_kl is not None:\n", + " if approx_kl > args.target_kl:\n", + " break" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.7" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/syllabus/examples/experimental/vecenv_rnn_lstm_pufferlib.py b/syllabus/examples/experimental/vecenv_rnn_lstm_pufferlib.py new file mode 100644 index 00000000..8c6ebde7 --- /dev/null +++ b/syllabus/examples/experimental/vecenv_rnn_lstm_pufferlib.py @@ -0,0 +1,806 @@ +# docs and experiment results can be found at https://docs.cleanrl.dev/rl-algorithms/ppo/#ppo_pettingzoo_ma_ataripy +import json +import queue +import random +import sys +import time +from dataclasses import dataclass +from typing import Dict, Tuple, TypeVar + +import gymnasium +import numpy as np +import torch +import torch.nn as nn +import torch.optim as optim +import tyro +from colorama import Fore, Style, init +from gymnasium import spaces +from pufferlib.emulation import PettingZooPufferEnv +from pufferlib.vector import Serial +from torch.distributions.categorical import Categorical +from tqdm import tqdm + +sys.path.append("../../..") +from lasertag import LasertagAdversarial # noqa: E402 +from syllabus.core import ( # noqa: E402 + DualCurriculumWrapper, + MultiagentSharedCurriculumWrapper, + PettingZooMultiProcessingSyncWrapper, + PettingZooTaskWrapper, + make_multiprocessing_curriculum, +) +from syllabus.curricula import ( # noqa: E402 + CentralizedPrioritizedLevelReplay, + DomainRandomization, + FictitiousSelfPlay, + PrioritizedFictitiousSelfPlay, + SelfPlay, +) +from syllabus.task_space.task_space import TaskSpace # noqa: E402 + +ActionType = TypeVar("ActionType") +AgentID = TypeVar("AgentID") +AgentType = TypeVar("AgentType") +EnvTask = TypeVar("EnvTask") +AgentTask = TypeVar("AgentTask") +ObsType = TypeVar("ObsType") + + +@dataclass +class Config: + # Experiment configuration + exp_name: str = "lasertag_" + wandb_project_name: str = "syllabus" + wandb_entity: str = "rpegoud" + logging_dir: str = "." + torch_deterministic: bool = True + cuda: bool = True + track: bool = False + capture_video: bool = False + + # Experiment setup + env_id: str = "lasertag" + total_timesteps: int = int(1e5) + seed: int = 0 + + # Algorithm specific arguments + gamma: float = 0.995 + gae_lambda: float = 0.95 + anneal_lr: bool = True + norm_adv: bool = True + target_kl = None # type: float + # Adam args + adam_lr: float = 1e-4 + adam_eps: float = 1e-5 + # PPO args + clip_coef: float = 0.2 + vf_coef: float = 0.5 + clip_vloss: bool = True + max_grad_norm: float = 0.5 + ent_coef: float = 0.0 + num_workers: int = 8 + num_minibatches: int = 4 + rollout_length: int = 256 + update_epochs: int = 5 + # Curricula + agent_curriculum = "FSP" + env_curriculum = "DR" + n_env_tasks = 4000 + max_agents = 10 + agent_update_frequency = 8000 + checkpoint_frequency = 4000 + + +def parse_args() -> Config: + init(autoreset=True) # Initialize colorama + config = tyro.cli(Config) + config.batch_size = int(config.num_workers * config.rollout_length) + print( + f"{Fore.RED}{Style.BRIGHT} Setting BATCH_SIZE to: NUM_WORKERS * ROLLOUT_LENGTH" + f"= {config.batch_size}" + ) + config.minibatch_size = int(config.batch_size // config.num_minibatches) + print( + f"{Fore.RED}{Style.BRIGHT} Setting MINIBATCH_SIZE to :" + f"BATCH_SIZE * NUM_MINIBATCHES= {config.minibatch_size}" + ) + + # log config + config_dict = config.__dict__ + print(f"{Fore.GREEN}{Style.BRIGHT} Config:") + print( + f"{Fore.GREEN}{Style.BRIGHT}Hyperparameters:" + f"{Style.NORMAL}{json.dumps(config_dict, sort_keys=True, indent=4)}{Style.RESET_ALL}" + ) + + print( + f"{Fore.BLUE}{Style.BRIGHT} RUNNING PPO ON {config.env_id} " + f"FOR {config.total_timesteps} TIMESTEPS ..." + ) + return config + + +def log_rewards(r_batch: torch.tensor, run, step: int) -> None: + r_logs = {f"agent_{agent_idx}": reward for agent_idx, reward in enumerate(r_batch)} + + run["charts/cumulative_rewards_per_update"].append(r_logs, step=step) + + +class Agent(nn.Module): + def __init__(self, obs_shape, n_actions) -> None: + super().__init__() + + self.conv = nn.Sequential( + self.layer_init( + nn.Conv2d( + obs_shape[0], + out_channels=16, + kernel_size=3, + stride=1, + ) + ), + nn.Flatten(), + nn.ReLU(), + ) + self.lstm = nn.LSTM(input_size=16 * 3 * 3, hidden_size=256, batch_first=True) + self.lstm_init() + + self.mlp = nn.Sequential( + nn.ReLU(), + self.layer_init(nn.Linear(256, 32)), + nn.ReLU(), + ) + self.actor = self.layer_init(nn.Linear(32, n_actions), scale=0.01) + self.critic = self.layer_init(nn.Linear(32, 1), scale=1) + + def get_states(self, x, lstm_states, done): + batch_size = x.size(0) # x shape: (num_envs, *obs_shape) + hidden = self.conv(x / 255.0) + hidden = hidden.view(batch_size, 1, -1) # shape: (num_envs, 1, n_features) + + # shape: (num_layers, batch_size, hidden_size) + h_states, c_states = lstm_states + + hidden, (new_h_states, new_c_states) = self.lstm(hidden, (h_states, c_states)) + + # Reset LSTM state if done + done = done.view(1, -1, 1) # (shape: 1, num_envs, 1) + new_h_states = new_h_states * (1 - done) + new_c_states = new_c_states * (1 - done) + + hidden = hidden.squeeze(1) + new_lstm_states = (new_h_states, new_c_states) + + return hidden, new_lstm_states + + def get_value(self, x, lstm_states, done): + hidden, _ = self.get_states(x, lstm_states, done) + hidden = self.mlp(hidden) + return self.critic(hidden) + + def get_action_and_value(self, x, lstm_states, done, action=None): + hidden, new_lstm_states = self.get_states(x, lstm_states, done) + hidden = self.mlp(hidden) + logits = self.actor(hidden) + probs = Categorical(logits=logits) + + if action is None: + action = probs.sample() + + return ( + action, + probs.log_prob(action), + probs.entropy(), + self.critic(hidden), + new_lstm_states, + ) + + def layer_init(self, layer, scale=np.sqrt(2), bias_const=0.0): + torch.nn.init.orthogonal_(layer.weight, scale) + torch.nn.init.constant_(layer.bias, bias_const) + return layer + + def lstm_init(self): + for name, param in self.lstm.named_parameters(): + if "bias" in name: + nn.init.constant_(param, 0) + elif "weight" in name: + nn.init.orthogonal_(param, 1.0) + + +def batchify(x, device=None): + """Converts PZ style returns to batch of torch arrays.""" + # convert to list of np arrays + x = np.stack([x[a] for a in x], axis=0) + # convert to torch + x = torch.tensor(x) + + if device is not None: + x = x.to(device) + + return x + + +def unbatchify(x, possible_agents: np.ndarray): + """Converts np array to PZ style arguments.""" + x = x.cpu().numpy() + x = {agent: x[idx] for idx, agent in enumerate(possible_agents)} + + return x + + +class LasertagParallelWrapper(PettingZooTaskWrapper): + """ + Wrapper ensuring compatibility with the PettingZoo Parallel API. + + Lasertag Environment: + * Action shape: `n_agents` * `Discrete(5)` + * Observation shape: Dict('image': Box(0, 255, (`n_agents`, 3, 5, 5), uint8)) + """ + + def __init__(self, n_agents, *args, **kwargs): + super().__init__(*args, **kwargs) + self.n_agents = n_agents + self.task = None + self.episode_return = 0 + self.possible_agents = [f"agent_{i}" for i in range(self.n_agents)] + self.env.agents = self.possible_agents + self.n_steps = 0 + self.env.render_mode = "human" + + def observation_space(self, agent): + env_space = self.env.observation_space["image"] + # Remove agent dimension + return gymnasium.spaces.Box( + low=env_space.low[0], + high=env_space.high[0], + shape=env_space.shape[1:], + dtype=env_space.dtype, + ) + + def action_space(self, agent): + return gymnasium.spaces.Discrete(5) + + def __getattr__(self, name): + """ + Delegate attribute lookup to the wrapped environment if the attribute + is not found in the LasertagParallelWrapper instance. + """ + return getattr(self.env, name) + + def _np_array_to_pz_dict(self, array: np.ndarray) -> Dict[str, np.ndarray]: + """ + Returns a dictionary containing individual observations for each agent. + Assumes that the batch dimension represents individual agents. + """ + out = {} + for idx, value in enumerate(array): + out[self.possible_agents[idx]] = value + return out + + def _singleton_to_pz_dict(self, value: bool) -> Dict[str, bool]: + """ + Broadcasts the `done` and `trunc` flags to dictionaries keyed by agent id. + """ + return {agent: value for agent in self.agents} + + def reset( + self, seed: int = None, **kwargs + ) -> Tuple[Dict[AgentID, ObsType], Dict[AgentID, dict]]: + """ + Resets the environment and returns a dictionary of observations + keyed by agent ID. + """ + if "new_task" in kwargs and kwargs["new_task"] is not None: + self.task = kwargs.pop("new_task") + seed = self.task[0] + self.env.seed(seed) + obs = self.env.reset_random() # random level generation + pz_obs = self._np_array_to_pz_dict(obs["image"]) + return pz_obs, {} + + def step( + self, + action: Dict[AgentID, ActionType], + ) -> Tuple[ + Dict[AgentID, ObsType], + Dict[AgentID, float], + Dict[AgentID, bool], + Dict[AgentID, bool], + Dict[AgentID, dict], + ]: + """ + Takes inputs in the PettingZoo (PZ) Parallel API format, performs a step and + returns outputs in PZ format. + """ + action = batchify(action) + obs, rew, done, info = self.env.step(action) + obs = obs["image"] + trunc = False # there is no `truncated` flag in this environment + self.task_completion = self._task_completion(obs, rew, done, trunc, info) + # convert outputs back to PZ format + obs, rew = map(self._np_array_to_pz_dict, [obs, rew]) + done, trunc, info = map( + self._singleton_to_pz_dict, [done, trunc, self.task_completion] + ) + info["agent_id"] = self.task[1] + self.n_steps += 1 + return self.observation(obs), rew, done, trunc, info + + +class AgentStorage: + """ + First-In Last-Out queue for agent storage in vectorized setups. + """ + + def __init__(self, size: int = 5) -> None: + self.size = size + self.agent_queue = queue.Queue(maxsize=size) + + def add_agent(self, agent: torch.nn.Module): + if self.agent_queue.full(): + oldest_agent = self.agent_queue.get() + del oldest_agent + self.agent_queue.put(agent) + + def get_agent_by_task(self, agent_task: int) -> torch.nn.Module: + """ + Returns the agent corresponding to the input agent task. + Agent task 0 refers to the latest added agent. + """ + assert ( + agent_task >= 0 and agent_task < self.size + ), f"Expected task in range [0, {self.size-1}], got {agent_task}" + # we want task 0 to return the newest agent + # and task ``size` to return the oldest + return self.agent_queue.queue[-(agent_task + 1)] + + +def split_batch(joint_obs: torch.Tensor) -> Tuple[torch.Tensor]: + """Splits a batch of joint data in agent and opponent data.""" + assert ( + joint_obs.shape[0] == args.num_workers * 2 + ), f"Expected shape {args.num_workers * 2}, got: {joint_obs.shape[0]}" + + agent_indices = [i for i in np.arange(0, args.num_workers * 2, 2)] + opp_indices = [i for i in np.arange(1, args.num_workers * 2, 2)] + + agent_data = joint_obs[agent_indices] + opp_data = joint_obs[opp_indices] + + return agent_data, opp_data + + +def reconstruct_batch( + agent_data: torch.Tensor, opponent_data: torch.Tensor, size: int +) -> torch.Tensor: + """Reconstructs a batch of joint data from agent and opponent data""" + batch = torch.zeros(size, dtype=agent_data.dtype) + batch[np.arange(0, size, 2)] = agent_data # even indices = agent + batch[np.arange(1, size, 2)] = opponent_data # odd indices = opponent + return batch + + +agent_curriculums = { + "SP": SelfPlay, + "FSP": FictitiousSelfPlay, + "PFSP": PrioritizedFictitiousSelfPlay, +} +env_curriculums = { + "DR": DomainRandomization, + "PLR": CentralizedPrioritizedLevelReplay, +} + + +def make_env_fn(components=None): + def thunk(): + env = LasertagAdversarial() + env = LasertagParallelWrapper(env=env, n_agents=2) + if components is not None: + env = PettingZooMultiProcessingSyncWrapper( + env, + components, + task_space=TaskSpace([args.n_env_tasks, args.max_agents]), + buffer_size=4, + ) + env = PettingZooPufferEnv(env) + return env + + return thunk + + +if __name__ == "__main__": + args = parse_args() + run_name = f"{args.env_id}__{args.exp_name}__{args.seed}__{int(time.time())}" + if args.track: + import neptune + from dotenv import dotenv_values + + env_variables = dotenv_values("credentials.env") + run = neptune.init_run( + project="rpegoud/syllabus", api_token=env_variables["NEPTUNE_API_KEY"] + ) + run["parameters"] = args.__dict__ + + # seeding + random.seed(args.seed) + np.random.seed(args.seed) + torch.manual_seed(args.seed) + torch.backends.cudnn.deterministic = args.torch_deterministic + + device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + + # agent and opponent selection indices + # joint obs, rewards, dones alternate between agent and opp + agent_indices = torch.arange(0, args.num_workers * 2, 2) + opponent_indices = torch.arange(1, args.num_workers * 2, 2) + + # agent setup + exemplar_env = make_env_fn(None)() + agent = Agent( + exemplar_env.observation_space("agent_0").shape, + exemplar_env.action_space("agent_0").n, + ).to(device) + optimizer = optim.Adam(agent.parameters(), lr=args.adam_lr, eps=args.adam_eps) + + # queue used to store concurrent versions of the student agent + agents_storage = AgentStorage(5) + agents_storage.add_agent(agent) + + # curriculum setup + env_task_space = TaskSpace(spaces.Discrete(args.n_env_tasks)) + env_curriculum_settings = { + "DR": {"task_space": env_task_space}, + "PLR": { + "task_space": env_task_space, + "num_steps": args.rollout_length, + "num_processes": args.num_workers, + "gamma": args.gamma, + "gae_lambda": args.gae_lambda, + "task_sampler_kwargs_dict": {"strategy": "value_l1"}, + }, + } + agent_curriculum_settings = { + "device": device, + "storage_path": f"{args.agent_curriculum}_agents", + "max_agents": args.max_agents, + "seed": args.seed, + } + + env_curriculum = env_curriculums[args.env_curriculum]( + **env_curriculum_settings[args.env_curriculum] + ) + env_curriculum = MultiagentSharedCurriculumWrapper( + env_curriculum, exemplar_env.possible_agents + ) + agent_curriculum = agent_curriculums[args.agent_curriculum]( + agent=agent, **agent_curriculum_settings + ) + curriculum = DualCurriculumWrapper( + env_curriculum, + agent_curriculum, + ) + curriculum = make_multiprocessing_curriculum(curriculum) + + # env setup + envs = [make_env_fn(curriculum.get_components()) for _ in range(args.num_workers)] + envs = Serial( + envs, + [() for _ in range(args.num_workers)], + [{} for _ in range(args.num_workers)], + args.num_workers, + ) + envs.is_vector_env = True + + # ALGO Logic: Storage setup + obs = torch.zeros( + (args.rollout_length, args.num_workers) + envs.single_observation_space.shape + ).to(device) + actions = torch.zeros( + (args.rollout_length, args.num_workers) + envs.single_action_space.shape + ).to(device) + dones = torch.zeros((args.rollout_length, args.num_workers)).to(device) + logprobs = torch.zeros((args.rollout_length, args.num_workers)).to(device) + rewards = torch.zeros((args.rollout_length, args.num_workers)).to(device) + values = torch.zeros((args.rollout_length, args.num_workers)).to(device) + + lstm_state = ( + torch.zeros(agent.lstm.num_layers, args.num_workers, agent.lstm.hidden_size).to( + device + ), + torch.zeros(agent.lstm.num_layers, args.num_workers, agent.lstm.hidden_size).to( + device + ), + ) + lstm_state_opp = ( + torch.zeros(agent.lstm.num_layers, args.num_workers, agent.lstm.hidden_size).to( + device + ), + torch.zeros(agent.lstm.num_layers, args.num_workers, agent.lstm.hidden_size).to( + device + ), + ) + + # TRY NOT TO MODIFY: start the game + global_step = 0 + start_time = time.time() + + num_updates = int(args.total_timesteps // args.batch_size) + env_tasks, agent_tasks = [], np.zeros(args.num_workers) + + cumulative_rewards = np.zeros(args.num_workers * 2) + next_obs, info = envs.reset() + + with tqdm(total=num_updates) as pbar: + for update in range(1, num_updates + 1): + # Annealing the rate if instructed to do so. + if args.anneal_lr: + frac = 1.0 - (update - 1.0) / num_updates + lrnow = frac * args.adam_lr + optimizer.param_groups[0]["lr"] = lrnow + + initial_lstm_state = (lstm_state[0].clone(), lstm_state[1].clone()) + next_obs = torch.Tensor(next_obs).to(device) + next_done = torch.zeros(args.num_workers).to(device) + # task = 0 + + for step in range(0, args.rollout_length): + global_step += 1 * args.num_workers * 2 + obs[step] = next_obs[agent_indices] + dones[step] = next_done + + # action selection + with torch.no_grad(): + agent_obs, opp_obs = split_batch(next_obs) + + # iterate over agent_tasks + for task in set(agent_tasks): + selected_agent = agents_storage.get_agent_by_task(int(task)) + + # create batches for each task + agent_task_indices = np.where(agent_tasks == task)[0] + next_done_batch = next_done[agent_task_indices] + agent_obs_batch = agent_obs[agent_task_indices] + opp_obs_batch = opp_obs[agent_task_indices] + + lstm_state_batch = ( + lstm_state[0][:, agent_task_indices], + lstm_state[1][:, agent_task_indices], + ) + + lstm_state_opp_batch = ( + lstm_state_opp[0][:, agent_task_indices], + lstm_state_opp[1][:, agent_task_indices], + ) + + # initialize + agent_actions = torch.zeros( + args.num_workers, dtype=torch.int32 + ).to(device) + opp_actions = torch.zeros( + args.num_workers, dtype=torch.int32 + ).to(device) + agent_value = torch.zeros(args.num_workers).to(device) + logprob = torch.zeros(args.num_workers).to(device) + + # learner action selection + ( + agent_actions_batch, + logprob_batch, + _, + agent_value_batch, + new_lstm_state_batch, + ) = selected_agent.get_action_and_value( + agent_obs_batch, lstm_state_batch, next_done_batch + ) + + # opponent action selection + ( + opp_actions_batch, + _, + _, + _, + new_lstm_state_opp_batch, + ) = selected_agent.get_action_and_value( + agent_obs_batch, lstm_state_opp_batch, next_done_batch + ) + + # reconstruct data from batches + agent_obs[agent_task_indices] = agent_obs_batch + agent_value[agent_task_indices] = ( + agent_value_batch.flatten().float() + ) + agent_actions[agent_task_indices] = agent_actions_batch.int() + opp_actions[agent_task_indices] = opp_actions_batch.int() + logprob[agent_task_indices] = logprob_batch.float() + next_done[agent_task_indices] = next_done_batch.float() + # print(agent_actions) + + # reconstruct the LSTM state + for i, idx in enumerate(agent_task_indices): + lstm_state[0][:, idx] = new_lstm_state_batch[0][:, i] + lstm_state[1][:, idx] = new_lstm_state_batch[1][:, i] + lstm_state_opp[0][:, idx] = new_lstm_state_opp_batch[0][ + :, i + ] + lstm_state_opp[1][:, idx] = new_lstm_state_opp_batch[1][ + :, i + ] + + values[step] = agent_value.flatten().cpu() + + joint_actions = reconstruct_batch( + agent_actions.cpu(), + opp_actions.cpu(), + size=args.num_workers * 2, + ) + next_obs, reward, next_done, trunc, info = envs.step( + joint_actions.numpy() + ) + agent_tasks = np.array([i["agent_id"] for i in info]) + print(agent_tasks) + + # task = agent_tasks[0] + rewards[step] = torch.tensor(reward[agent_indices]).to(device).view(-1) + next_obs = torch.Tensor(next_obs).to(device) + next_done = torch.Tensor(next_done[agent_indices]).to(device) + + actions[step] = agent_actions.cpu() + logprobs[step] = logprob.cpu() + + cumulative_rewards = cumulative_rewards + reward + if args.track: + log_rewards(cumulative_rewards, run, global_step) + + # generalized advantage estimation (for the learning agent only) + with torch.no_grad(): + agent_obs, opp_obs = split_batch(next_obs) + next_value = agent.get_value(agent_obs, lstm_state, next_done).reshape( + 1, -1 + ) + advantages = torch.zeros(args.rollout_length, args.num_workers).to( + device + ) + + lastgaelam = 0 + for t in reversed(range(args.rollout_length)): + if t == args.rollout_length - 1: + nextnonterminal = 1.0 - next_done + nextvalues = next_value + else: + nextnonterminal = 1.0 - dones[t + 1] + nextvalues = values[t + 1] + delta = ( + rewards[t] + + args.gamma * nextvalues * nextnonterminal + - values[t] + ) + advantages[t] = lastgaelam = ( + delta + + args.gamma * args.gae_lambda * nextnonterminal * lastgaelam + ) + returns = advantages + values + + # flatten the batch + b_obs = obs.reshape((-1,) + envs.single_observation_space.shape) + b_dones = dones.reshape((-1,)) + b_logprobs = logprobs.reshape(-1) + b_actions = actions.reshape((-1,) + envs.single_action_space.shape) + b_advantages = advantages.reshape(-1) + b_returns = returns.reshape(-1) + b_values = values.reshape(-1) + print("optimizing") + # Optimizing the policy and value network + b_inds = np.arange(args.batch_size) + clipfracs = [] + for epoch in range(args.update_epochs): + np.random.shuffle(b_inds) + for start in range(0, args.batch_size, args.minibatch_size): + end = start + args.minibatch_size + batch_index = b_inds[start:end] + + b_lstm_state = ( + initial_lstm_state[0].repeat( + 1, args.minibatch_size // args.num_workers, 1 + ), + initial_lstm_state[1].repeat( + 1, args.minibatch_size // args.num_workers, 1 + ), + ) # TODO: instead of a common initialization for each batch item, + # retrieve the training lstm state related to the item? + + _, newlogprob, entropy, newvalue, b_lstm_state = ( + agent.get_action_and_value( + b_obs[batch_index], + b_lstm_state, + b_dones[batch_index], + b_actions.long()[batch_index], + ) + ) + logratio = newlogprob - b_logprobs[batch_index] + ratio = logratio.exp() + + with torch.no_grad(): + # calculate approx_kl http://joschu.net/blog/kl-approx.html + old_approx_kl = (-logratio).mean() + approx_kl = ((ratio - 1) - logratio).mean() + clipfracs += [ + ((ratio - 1.0).abs() > args.clip_coef).float().mean().item() + ] + + mb_advantages = b_advantages[batch_index] + if args.norm_adv: + mb_advantages = (mb_advantages - mb_advantages.mean()) / ( + mb_advantages.std() + 1e-8 + ) + + # Policy loss + pg_loss1 = -mb_advantages * ratio + pg_loss2 = -mb_advantages * torch.clamp( + ratio, 1 - args.clip_coef, 1 + args.clip_coef + ) + pg_loss = torch.max(pg_loss1, pg_loss2).mean() + + # Value loss + newvalue = newvalue.view(-1) + if args.clip_vloss: + v_loss_unclipped = (newvalue - b_returns[batch_index]) ** 2 + v_clipped = b_values[batch_index] + torch.clamp( + newvalue - b_values[batch_index], + -args.clip_coef, + args.clip_coef, + ) + v_loss_clipped = (v_clipped - b_returns[batch_index]) ** 2 + v_loss_max = torch.max(v_loss_unclipped, v_loss_clipped) + v_loss = 0.5 * v_loss_max.mean() + else: + v_loss = 0.5 * ((newvalue - b_returns[batch_index]) ** 2).mean() + + entropy_loss = entropy.mean() + loss = ( + pg_loss - args.ent_coef * entropy_loss + v_loss * args.vf_coef + ) + + optimizer.zero_grad() + loss.backward() + nn.utils.clip_grad_norm_(agent.parameters(), args.max_grad_norm) + optimizer.step() + + if args.target_kl is not None: + if approx_kl > args.target_kl: + break + + print("Update agent") + curriculum.update_agent(agent) + agents_storage.add_agent(agent) + + y_pred, y_true = b_values.cpu().numpy(), b_returns.cpu().numpy() + var_y = np.var(y_true) + explained_var = ( + np.nan if var_y == 0 else 1 - np.var(y_true - y_pred) / var_y + ) + + if args.track: + run["charts/learning_rate"].append( + optimizer.param_groups[0]["lr"], step=global_step + ) + run["losses/value_loss"].append(v_loss.item(), step=global_step) + run["losses/policy_loss"].append(pg_loss.item(), step=global_step) + run["losses/entropy"].append(entropy_loss.item(), step=global_step) + run["losses/old_approx_kl"].append( + old_approx_kl.item(), step=global_step + ) + run["losses/approx_kl"].append(approx_kl.item(), step=global_step) + run["losses/clipfrac"].append(np.mean(clipfracs), step=global_step) + run["losses/explained_variance"].append(explained_var, step=global_step) + + print( + f"{Fore.WHITE}{Style.BRIGHT} Steps per second:", + int(global_step / (time.time() - start_time)), + ) + pbar.update(1) + + envs.close() + if args.track: + run.stop() + run.stop() diff --git a/syllabus/task_space/task_space.py b/syllabus/task_space/task_space.py index ce3ee150..0f40cefb 100644 --- a/syllabus/task_space/task_space.py +++ b/syllabus/task_space/task_space.py @@ -2,10 +2,18 @@ from typing import Any, List, Union import numpy as np -from gymnasium.spaces import Box, Dict, Discrete, MultiBinary, MultiDiscrete, Space, Tuple - - -class TaskSpace(): +from gymnasium.spaces import ( + Box, + Dict, + Discrete, + MultiBinary, + MultiDiscrete, + Space, + Tuple, +) + + +class TaskSpace: def __init__(self, gym_space: Union[Space, int], tasks=None): if not isinstance(gym_space, Space): @@ -41,8 +49,8 @@ def _create_gym_space(self, gym_space): elif isinstance(gym_space, list): # Syntactic sugar for tuple space spaces = [] - for i, value in enumerate(gym_space): - spaces[i] = self._create_gym_space(value) + for value in gym_space: + spaces.append(self._create_gym_space(value)) gym_space = Tuple(spaces) elif isinstance(gym_space, dict): # Syntactic sugar for dict space @@ -60,61 +68,112 @@ def _generate_task_names(self, gym_space): elif isinstance(gym_space, Tuple): tasks = [self._generate_task_names(value) for value in gym_space.spaces] elif isinstance(gym_space, Dict): - tasks = {key: tuple(self._generate_task_names(value)) for key, value in gym_space.spaces.items()} + tasks = { + key: tuple(self._generate_task_names(value)) + for key, value in gym_space.spaces.items() + } else: tasks = None return tasks def _make_task_encoder(self, space, tasks): if isinstance(space, Discrete): - assert space.n == len(tasks), f"Number of tasks ({space.n}) must match number of discrete options ({len(tasks)})" - self._encode_map = {task: i for i, task in enumerate(tasks)} - self._decode_map = {i: task for i, task in enumerate(tasks)} - encoder = lambda task: self._encode_map[task] if task in self._encode_map else None - decoder = lambda task: self._decode_map[task] if task in self._decode_map else None + assert space.n == len( + tasks + ), f"Number of tasks ({space.n}) must match number of discrete options ({len(tasks)})" + encode_map = {task: i for i, task in enumerate(tasks)} + decode_map = {i: task for i, task in enumerate(tasks)} + + def encoder(task): + return encode_map[task] if task in encode_map else None + + def decoder(task): + return decode_map[task] if task in decode_map else None elif isinstance(space, Box): - encoder = lambda task: task if space.contains(np.asarray(task, dtype=space.dtype)) else None - decoder = lambda task: task if space.contains(np.asarray(task, dtype=space.dtype)) else None + + def encoder(task): + return ( + task + if space.contains(np.asarray(task, dtype=space.dtype)) + else None + ) + + def decoder(task): + return ( + task + if space.contains(np.asarray(task, dtype=space.dtype)) + else None + ) + elif isinstance(space, Tuple): - assert len(space.spaces) == len(tasks), f"Number of task ({len(space.spaces)})must match options in Tuple ({len(tasks)})" - results = [list(self._make_task_encoder(s, t)) for (s, t) in zip(space.spaces, tasks)] + assert len(space.spaces) == len( + tasks + ), f"Number of task ({len(space.spaces)})must match options in Tuple ({len(tasks)})" + results = [ + list(self._make_task_encoder(s, t)) + for (s, t) in zip(space.spaces, tasks) + ] encoders = [r[0] for r in results] decoders = [r[1] for r in results] - encoder = lambda task: [e(t) for e, t in zip(encoders, task)] - decoder = lambda task: [d(t) for d, t in zip(decoders, task)] + + def encoder(task): + return [e(t) for e, t in zip(encoders, task)] + + def decoder(task): + return [d(t) for d, t in zip(decoders, task)] elif isinstance(space, MultiDiscrete): - assert len(space.nvec) == len(tasks), f"Number of steps in a tasks ({len(space.nvec)}) must match number of discrete options ({len(tasks)})" + assert len(space.nvec) == len( + tasks + ), f"Number of steps in a tasks ({len(space.nvec)}) must match number of discrete options ({len(tasks)})" combinations = [p for p in itertools.product(*tasks)] encode_map = {task: i for i, task in enumerate(combinations)} decode_map = {i: task for i, task in enumerate(combinations)} - encoder = lambda task: encode_map[task] if task in encode_map else None - decoder = lambda task: decode_map[task] if task in decode_map else None + def encoder(task): + return encode_map[task] if task in encode_map else None + + def decoder(task): + return decode_map[task] if task in decode_map else None elif isinstance(space, Dict): def helper(task, spaces, tasks, action="encode"): # Iteratively encodes or decodes each space in the dictionary output = {} - if (isinstance(spaces, dict) or isinstance(spaces, Dict)): + if isinstance(spaces, dict) or isinstance(spaces, Dict): for key, value in spaces.items(): - if (isinstance(value, dict) or isinstance(value, Dict)): + if isinstance(value, dict) or isinstance(value, Dict): temp = helper(task[key], value, tasks[key], action) output.update({key: temp}) else: - encoder, decoder = self._make_task_encoder(value, tasks[key]) - output[key] = encoder(task[key]) if action == "encode" else decoder(task[key]) + encoder, decoder = self._make_task_encoder( + value, tasks[key] + ) + output[key] = ( + encoder(task[key]) + if action == "encode" + else decoder(task[key]) + ) return output - encoder = lambda task: helper(task, space.spaces, tasks, "encode") - decoder = lambda task: helper(task, space.spaces, tasks, "decode") + def encoder(task): + return helper(task, space.spaces, tasks, "encode") + + def decoder(task): + return helper(task, space.spaces, tasks, "decode") + else: - encoder = lambda task: task - decoder = lambda task: task + + def encoder(task): + return task + + def decoder(task): + return task + return encoder, decoder def decode(self, encoding): @@ -146,7 +205,9 @@ def _enumerate_axes(self, list_or_size: Union[np.ndarray, int]): if isinstance(list_or_size, int) or isinstance(list_or_size, np.int64): return tuple(range(list_or_size)) elif isinstance(list_or_size, list) or isinstance(list_or_size, np.ndarray): - return tuple(itertools.product(*[self._enumerate_axes(x) for x in list_or_size])) + return tuple( + itertools.product(*[self._enumerate_axes(x) for x in list_or_size]) + ) else: raise NotImplementedError(f"{type(list_or_size)}") @@ -160,7 +221,9 @@ def tasks(self) -> List[Any]: return list(range(len(self._task_list))) return self._task_list - def get_tasks(self, gym_space: Space = None, sample_interval: float = None) -> List[tuple]: + def get_tasks( + self, gym_space: Space = None, sample_interval: float = None + ) -> List[tuple]: """ Return the full list of discrete tasks in the task_space. Return a sample of the tasks for continuous spaces if sample_interval is specified. @@ -174,9 +237,15 @@ def get_tasks(self, gym_space: Space = None, sample_interval: float = None) -> L elif isinstance(gym_space, Box): raise NotImplementedError elif isinstance(gym_space, Tuple): - return list(itertools.product([self.get_tasks(task_space=s) for s in gym_space.spaces])) + return list( + itertools.product( + [self.get_tasks(task_space=s) for s in gym_space.spaces] + ) + ) elif isinstance(gym_space, Dict): - return itertools.product([self.get_tasks(task_space=s) for s in gym_space.spaces.values()]) + return itertools.product( + [self.get_tasks(task_space=s) for s in gym_space.spaces.values()] + ) elif isinstance(gym_space, MultiBinary): return list(self._enumerate_axes(gym_space.nvec)) elif isinstance(gym_space, MultiDiscrete): @@ -208,7 +277,9 @@ def count_tasks(self, gym_space: Space = None) -> int: elif isinstance(gym_space, Tuple): return sum([self.count_tasks(gym_space=s) for s in gym_space.spaces]) elif isinstance(gym_space, Dict): - return sum([self.count_tasks(gym_space=s) for s in gym_space.spaces.values()]) + return sum( + [self.count_tasks(gym_space=s) for s in gym_space.spaces.values()] + ) elif isinstance(gym_space, MultiBinary): return TaskSpace._sum_axes(gym_space.nvec) elif isinstance(gym_space, MultiDiscrete): @@ -226,7 +297,9 @@ def contains(self, task): def increase_space(self, amount: Union[int, float] = 1): if isinstance(self.gym_space, Discrete): - assert isinstance(amount, int), f"Discrete task space can only be increased by integer amount. Got {amount} instead." + assert isinstance( + amount, int + ), f"Discrete task space can only be increased by integer amount. Got {amount} instead." return Discrete(self.gym_space.n + amount) def sample(self): @@ -249,4 +322,7 @@ def box_contains(self, x) -> bool: except (ValueError, TypeError): return False - return not bool(x.shape == self.gym_space.shape and np.any((x < self.gym_space.low) | (x > self.gym_space.high))) + return not bool( + x.shape == self.gym_space.shape + and np.any((x < self.gym_space.low) | (x > self.gym_space.high)) + ) diff --git a/syllabus/tests/sync_test_env.py b/syllabus/tests/sync_test_env.py index 808c991e..fb733b30 100644 --- a/syllabus/tests/sync_test_env.py +++ b/syllabus/tests/sync_test_env.py @@ -1,8 +1,12 @@ import warnings +from copy import copy + import gymnasium as gym -from syllabus.core import TaskEnv, PettingZooTaskEnv + +from syllabus.core import PettingZooTaskEnv, TaskEnv +from syllabus.task_space import TaskSpace +from syllabus.core import PettingZooTaskEnv, TaskEnv from syllabus.task_space import TaskSpace -from copy import copy class SyncTestEnv(TaskEnv): @@ -10,13 +14,20 @@ def __init__(self, num_episodes, num_steps=100): super().__init__() self.num_steps = num_steps self.action_space = gym.spaces.Discrete(2) - self.observation_space = gym.spaces.Tuple((gym.spaces.Discrete(self.num_steps), gym.spaces.Discrete(2))) - self.task_space = TaskSpace(gym.spaces.Discrete(num_episodes + 1), ["error task"] + [f"task {i+1}" for i in range(num_episodes)]) + self.observation_space = gym.spaces.Tuple( + (gym.spaces.Discrete(self.num_steps), gym.spaces.Discrete(2)) + ) + self.task_space = TaskSpace( + gym.spaces.Discrete(num_episodes + 1), + ["error task"] + [f"task {i+1}" for i in range(num_episodes)], + ) self.task = "error_task" def reset(self, new_task=None): if new_task == "error task": - warnings.warn("Received error task. This likely means that too many tasks are being requested.") + warnings.warn( + "Received error task. This likely means that too many tasks are being requested." + ) if new_task is None: warnings.warn("No task provided. Resetting to error task.") self.task = new_task @@ -30,7 +41,10 @@ def step(self, action): rew = 1 term = self._turn >= self.num_steps trunc = False - info = {"content": "step", "task_completion": self._task_completion(obs, rew, term, trunc, {})} + info = { + "content": "step", + "task_completion": self._task_completion(obs, rew, term, trunc, {}), + } return obs, rew, term, trunc, info @@ -41,7 +55,7 @@ def __init__(self, num_episodes, num_steps=100): self.possible_agents = ["agent1", "agent2"] self._action_spaces = {agent: gym.spaces.Discrete(2) for agent in self.possible_agents} self.observation_spaces = {agent: gym.spaces.Tuple((gym.spaces.Discrete(self.num_steps), gym.spaces.Discrete(2))) - for agent in self.possible_agents} + for agent in self.possible_agents} self.task_space = TaskSpace(gym.spaces.Discrete(num_episodes + 1), ["error task"] + [f"task {i+1}" for i in range(num_episodes)]) self.task = "error_task" self.metadata = {"render.modes": ["human"]} @@ -52,7 +66,11 @@ def action_space(self, agent): def reset(self, new_task=None): self.agents = copy(self.possible_agents) if new_task == "error task": - print(ValueError("Received error task. This likely means that too many tasks are being requested.")) + print( + ValueError( + "Received error task. This likely means that too many tasks are being requested." + ) + ) self.task = new_task self._turn = 0 obs = {agent: 0.5 for agent in self.agents} @@ -62,12 +80,22 @@ def reset(self, new_task=None): def step(self, action): self._turn += 1 - obs = {agent: self.observation((self._turn, action[agent])) for agent in self.agents} + obs = { + agent: self.observation((self._turn, action[agent])) + for agent in self.agents + } rew = {agent: 1 for agent in self.agents} term = {agent: self._turn >= self.num_steps for agent in self.agents} trunc = {agent: False for agent in self.agents} - info = {agent: {"content": "step", "task_completion": self._task_completion(obs, rew, all(term.values()), all(trunc.values()), {})} - for agent in self.agents} + info = { + agent: { + "content": "step", + "task_completion": self._task_completion( + obs, rew, all(term.values()), all(trunc.values()), {} + ), + } + for agent in self.agents + } if all(term.values()) or all(trunc.values()): self.agents = [] return obs, rew, term, trunc, info diff --git a/syllabus/tests/utils.py b/syllabus/tests/utils.py index 20cbb20e..2935db47 100644 --- a/syllabus/tests/utils.py +++ b/syllabus/tests/utils.py @@ -13,7 +13,7 @@ from syllabus.core import MultiProcessingSyncWrapper, PettingZooMultiProcessingSyncWrapper, RaySyncWrapper, PettingZooRaySyncWrapper, ReinitTaskWrapper, PettingZooReinitTaskWrapper from syllabus.examples.task_wrappers.cartpole_task_wrapper import CartPoleTaskWrapper from syllabus.task_space import TaskSpace -from syllabus.tests import SyncTestEnv, PettingZooSyncTestEnv +from syllabus.tests import PettingZooSyncTestEnv, SyncTestEnv def evaluate_random_policy(make_env, num_episodes=100, seeds=None): @@ -303,6 +303,7 @@ def create_cartpole_env(*args, type=None, env_args=(), env_kwargs={}, **kwargs): # Nethack Tests def create_nethack_env(*args, type=None, env_args=(), env_kwargs={}, **kwargs): from nle.env.tasks import NetHackScore + from syllabus.examples.task_wrappers.nethack_wrappers import NethackTaskWrapper env = NetHackScore(*env_args, **env_kwargs) @@ -323,8 +324,17 @@ def create_procgen_env(*args, type=None, env_args=(), env_kwargs={}, **kwargs): try: import procgen - from syllabus.examples.task_wrappers.procgen_task_wrapper import \ - ProcgenTaskWrapper + from syllabus.examples.task_wrappers.procgen_task_wrapper import ( + ProcgenTaskWrapper, + ) +# Procgen Tests +def create_procgen_env(*args, type=None, env_args=(), env_kwargs={}, **kwargs): + try: + import procgen + + from syllabus.examples.task_wrappers.procgen_task_wrapper import ( + ProcgenTaskWrapper, + ) except ImportError: warnings.warn("Unable to import procgen.")
ParameterValue
{key}{value}