From 232f081f8d49715498cf5a67bacb173123b3a58b Mon Sep 17 00:00:00 2001 From: techmore Date: Mon, 16 Oct 2023 14:01:57 -0400 Subject: [PATCH 01/12] Update run_baseline_parallel.py The code now checks how much cpus the dev has and set the cpu_num equal to that amount. --- baselines/run_baseline_parallel.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/baselines/run_baseline_parallel.py b/baselines/run_baseline_parallel.py index bb49f61e5..39679e226 100644 --- a/baselines/run_baseline_parallel.py +++ b/baselines/run_baseline_parallel.py @@ -8,6 +8,7 @@ from stable_baselines3.common.utils import set_random_seed from stable_baselines3.common.callbacks import CheckpointCallback from argparse_pokemon import * +import os def make_env(rank, env_conf, seed=0): """ @@ -39,8 +40,10 @@ def _init(): } env_config = change_env(env_config, args) - - num_cpu = 44 #64 #46 # Also sets the number of episodes per training iteration + + # Set the number of cpus dynamically + num_cpu = os.cpu_count() + env = SubprocVecEnv([make_env(i, env_config) for i in range(num_cpu)]) checkpoint_callback = CheckpointCallback(save_freq=ep_length, save_path=sess_path, @@ -62,4 +65,4 @@ def _init(): model = PPO('CnnPolicy', env, verbose=1, n_steps=ep_length, batch_size=512, n_epochs=1, gamma=0.999) for i in range(learn_steps): - model.learn(total_timesteps=(ep_length)*num_cpu*1000, callback=checkpoint_callback) \ No newline at end of file + model.learn(total_timesteps=(ep_length)*num_cpu*1000, callback=checkpoint_callback) From 3358a17a61697156ab3bf8ee1735d630b463a052 Mon Sep 17 00:00:00 2001 From: techmore Date: Mon, 23 Oct 2023 08:00:40 -0400 Subject: [PATCH 02/12] add www folder this adds a folder and files to run a flask app to accept checkpoint uploads, sort and simply them --- www/app.py | 92 ++++++++++++++++++++++++++++++++++++++++ www/templates/index.html | 43 +++++++++++++++++++ www/uploads/metadata.txt | 0 3 files changed, 135 insertions(+) create mode 100644 www/app.py create mode 100644 www/templates/index.html create mode 100644 www/uploads/metadata.txt diff --git a/www/app.py b/www/app.py new file mode 100644 index 000000000..0c5e1248b --- /dev/null +++ b/www/app.py @@ -0,0 +1,92 @@ +import os +import json +from flask import Flask, request, jsonify, render_template +from werkzeug.utils import secure_filename +import hashlib +from datetime import datetime +from flask import send_from_directory +import re # Import the regular expression module + +app = Flask(__name__) + +app.config['UPLOAD_FOLDER'] = './uploads' + +# In-memory list to hold file metadata +files_data = [] + +@app.route('/metadata.txt') +def metadata(): + try: + return send_from_directory('./uploads', 'metadata.txt') + except FileNotFoundError: + return "metadata.txt not found.", 404 + +@app.route('/highest_steps') +def highest_steps(): + read_metadata() # Read metadata from file + + if not files_data: + return "No files uploaded.", 404 + + highest_steps_file = max(files_data, key=lambda x: x['steps']) + directory, filename = os.path.split(highest_steps_file['filepath']) + return send_from_directory(directory=directory, filename=filename) + +# Function to read metadata from file +def read_metadata(): + global files_data + try: + with open('./uploads/metadata.txt', 'r') as f: + files_data = json.load(f) + except: + files_data = [] + +# Function to write metadata to file +def write_metadata(data): + with open('./uploads/metadata.txt', 'w') as f: + json.dump(data, f) + +# Read metadata from file on startup +read_metadata() + +@app.route('/') +def index(): + read_metadata() # Read metadata from file + sorted_files = sorted(files_data, key=lambda x: x['steps'], reverse=True) + return render_template('index.html', files=sorted_files) + +# ... + +@app.route('/upload', methods=['POST']) +def upload_file(): + global files_data + + uploaded_file = request.files['file'] + timestamp = datetime.now().strftime("%Y%m%d%H%M%S") + sha1 = hashlib.sha1(uploaded_file.read()).hexdigest()[:10] + uploaded_file.seek(0) + original_filename = secure_filename(uploaded_file.filename) + + # Extract the 'steps' from the original filename + match = re.search(r'poke_(\d+)_steps\.zip', original_filename) + if match: + steps = int(match.group(1)) + else: + steps = None # Default value if not found + + filename = f"poke_{steps}_steps_{sha1}_{timestamp}" + filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename) + + uploaded_file.save(filepath) + + file_info = {'filename': filename, 'filepath': filepath, 'timestamp': timestamp, 'steps': steps} + files_data.append(file_info) + + files_data.sort(key=lambda x: x.get('steps', 0), reverse=True) + write_metadata(files_data) + + return jsonify({'success': True}) + +if __name__ == '__main__': + app.run(debug=True) + diff --git a/www/templates/index.html b/www/templates/index.html new file mode 100644 index 000000000..62314dd0a --- /dev/null +++ b/www/templates/index.html @@ -0,0 +1,43 @@ + + + + + File Uploads + + + +
+

AI Plays Pokemon

+ + Download File with Highest Steps + + + + + + + + + + + + {% for file in files %} + + + + + + + {% endfor %} + +
StepsFilenameTimestampDownload
{{ file.steps }}{{ file.filename }}{{ file.timestamp }}Download
+
+ + + diff --git a/www/uploads/metadata.txt b/www/uploads/metadata.txt new file mode 100644 index 000000000..e69de29bb From 1c70c2eb62259ff71483ded14b16141ecc7e8e17 Mon Sep 17 00:00:00 2001 From: techmore Date: Mon, 23 Oct 2023 15:09:54 -0400 Subject: [PATCH 03/12] Update app.py --- www/app.py | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/www/app.py b/www/app.py index 0c5e1248b..e6592d22e 100644 --- a/www/app.py +++ b/www/app.py @@ -57,6 +57,10 @@ def index(): # ... +@app.route('/uploads/') +def download_file(filename): + return send_from_directory(app.config['UPLOAD_FOLDER'], filename) + @app.route('/upload', methods=['POST']) def upload_file(): global files_data @@ -74,19 +78,32 @@ def upload_file(): else: steps = None # Default value if not found - filename = f"poke_{steps}_steps_{sha1}_{timestamp}" + filename = f"poke_{steps}_steps_{sha1}_{timestamp}.zip" filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename) - uploaded_file.save(filepath) + # Check if a file with the same 'steps' value already exists + existing_entry = next((entry for entry in files_data if entry['steps'] == steps), None) + + if existing_entry: + # Update the existing entry + existing_entry['filename'] = filename + existing_entry['filepath'] = filepath + existing_entry['timestamp'] = timestamp + else: + # Create a new entry + file_info = {'filename': filename, 'filepath': filepath, 'timestamp': timestamp, 'steps': steps} + files_data.append(file_info) - file_info = {'filename': filename, 'filepath': filepath, 'timestamp': timestamp, 'steps': steps} - files_data.append(file_info) + # Save the uploaded file to the specified filepath + uploaded_file.save(filepath) + # Sort the metadata by 'steps' in reverse order files_data.sort(key=lambda x: x.get('steps', 0), reverse=True) write_metadata(files_data) return jsonify({'success': True}) + if __name__ == '__main__': app.run(debug=True) From 8e2e2f63d3a59a9c1e7c3c633eae155cddf0b58e Mon Sep 17 00:00:00 2001 From: techmore Date: Mon, 23 Oct 2023 15:14:52 -0400 Subject: [PATCH 04/12] Create menu.py This is adding restore and upload functionality from remote devices and local devices --- baselines/menu.py | 156 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 baselines/menu.py diff --git a/baselines/menu.py b/baselines/menu.py new file mode 100644 index 000000000..51fcb29a6 --- /dev/null +++ b/baselines/menu.py @@ -0,0 +1,156 @@ +import argparse +import os +import re +import requests +import json +import subprocess + +def parse_args(): + parser = argparse.ArgumentParser(description='Your program description') + parser.add_argument('--menu', action='store_true', help='Show the menu') + parser.add_argument('--restore', help='Restore from a URL or use the default URL') + parser.add_argument('--upload', help='Upload to a URL or use the default URL') + return parser.parse_args() + +def list_all_sessions_and_pokes(): + all_folders = os.listdir() + session_folders = [folder for folder in all_folders if re.match(r'session_[0-9a-fA-F]{8}', folder)] + session_dict = {} + + for session_folder in session_folders: + poke_files = glob.glob(f"{session_folder}/poke_*_steps.zip") + if poke_files: + largest_poke_file = max(poke_files, key=lambda x: int(re.search(r'poke_(\d+)_steps', x).group(1))) + largest_step = int(re.search(r'poke_(\d+)_steps', largest_poke_file).group(1)) + session_dict[session_folder] = largest_step + + # Add downloaded checkpoints to the session_dict + downloaded_checkpoints = os.listdir('downloaded_checkpoints') + for downloaded_checkpoint in downloaded_checkpoints: + if downloaded_checkpoint.endswith('.zip'): + session_name = 'downloaded_checkpoints/' + downloaded_checkpoint + print('downloaded_checkpoints/' + downloaded_checkpoint) + session_dict[session_name] = 'downloaded_checkpoints/' + downloaded_checkpoint + #largest_step = int(re.search(r'poke_(\d+)_steps', downloaded_checkpoint).group(1)) + session_dict[session_name] = largest_step + + sorted_session_dict = {k: v for k, v in sorted(session_dict.items(), key=lambda item: item[1], reverse=True)} + return sorted_session_dict + +def remote_actions(): + url = input("Enter the URL for resuming or leave it blank for the default: ") + if url.strip() == "": + response = requests.get("http://127.0.0.1:5000/metadata.txt") + if response.status_code != 200: + print("Failed to fetch metadata from the server.") + return None + + server_metadata = response.text.strip() + + if not server_metadata: + print("No checkpoint metadata found. Is this an empty server?") + return None + + try: + server_metadata = json.loads(server_metadata) + except json.decoder.JSONDecodeError as e: + print("Error decoding JSON:", str(e)) + return None + + print(f"\nAvailable checkpoints from the server:") + for i, entry in enumerate(server_metadata): + print(f"{i + 1}. {entry['filename']}") + + server_selection = int(input("Enter the number of the checkpoint you want to download: ")) + download_directory = "downloaded_checkpoints" + os.makedirs(download_directory, exist_ok=True) + + download_response = requests.get(f"http://127.0.0.1:5000/uploads/{server_metadata[server_selection - 1]['filename']}") + if download_response.status_code != 200: + print("Failed to download the selected checkpoint.") + return None + + selected_server_entry = server_metadata[server_selection - 1]['filename'] + with open(f"downloaded_checkpoints/{selected_server_entry}", 'wb') as f: + f.write(download_response.content) + return f"downloaded_checkpoints/{selected_server_entry}" + +def show_menu(sess_path): + selected_checkpoint = None + session_dict = list_all_sessions_and_pokes() + + if not session_dict: + print("No checkpoints found.") + return selected_checkpoint + + print(f"\nAvailable sessions sorted by their largest checkpoints:") + for i, (session, largest_step) in enumerate(session_dict.items()): + print(f" {i + 1}. {session}/poke-{largest_step}_steps.zip") + + print("\n 95. Future-Delete Saved Files") + print(" 96. Resume from remote") + print(" 97. Upload to remote") + print(" 98. Exit") + print(" 99. Start a new run") + menu_selection = input("Enter the number of the menu option: ") + + if menu_selection == '96': + selected_checkpoint = remote_actions() + elif menu_selection.isdigit(): + # If a numeric option is selected, try to use it as a checkpoint + selection = int(menu_selection) + if 1 <= selection <= len(session_dict): + selected_session = list(session_dict.keys())[selection - 1] + selected_step = session_dict[selected_session] + selected_checkpoint = f"{selected_session}/poke_{selected_step}_steps.zip" + else: + print("Invalid selection.") + elif menu_selection == '97': + selection = input("Enter your selection for remote upload: ") + upload(selection, session_dict) + elif menu_selection == '98': + print("Exiting the menu.") + elif menu_selection == '99': + selected_checkpoint = None + else: + print("Invalid selection.") + return selected_checkpoint + + return selected_checkpoint + +# Define a function to restore from a URL or the default URL +def restore(url, download_selection): + response = requests.get(url) + + if response.status_code == 200: + filename = url.split("/")[-1] + with open(filename, 'wb') as file: + file.write(response.content) + print(f"Downloaded checkpoint: {filename}") + return filename + else: + print("Failed to download checkpoint.") + return None + +# Define a function to upload to a URL or the default URL +def upload(selection, session_dict): + try: + upload_selection = int(selection) + selected_session = list(session_dict.keys())[upload_selection - 1] + selected_step = session_dict[selected_session] + file_path = f"{selected_session}/poke_{selected_step}_steps.zip" + + print(file_path) + upload_command = f"curl -X POST -F file=@{file_path} http://127.0.0.1:5000/upload" + subprocess.run(upload_command, shell=True) + except (ValueError, IndexError): + print("Invalid selection.") + +if __name__ == '__main__': + args = parse_args() + if args.menu: + selected_checkpoint = show_menu(sess_path) + if args.restore and selected_checkpoint is None: + selected_checkpoint = restore(args.restore, None) + if args.upload: + upload(args.upload, None) From 2c2cb66c8be6054c3899c63f1e2d5689cbd32303 Mon Sep 17 00:00:00 2001 From: techmore Date: Mon, 23 Oct 2023 16:59:10 -0400 Subject: [PATCH 05/12] Update app.py --- www/app.py | 63 ++++++++++++++++++++++++------------------------------ 1 file changed, 28 insertions(+), 35 deletions(-) diff --git a/www/app.py b/www/app.py index e6592d22e..1641e12d9 100644 --- a/www/app.py +++ b/www/app.py @@ -1,49 +1,39 @@ import os import json -from flask import Flask, request, jsonify, render_template +from flask import Flask, request, jsonify, render_template, send_from_directory from werkzeug.utils import secure_filename import hashlib from datetime import datetime -from flask import send_from_directory -import re # Import the regular expression module +import re app = Flask(__name__) app.config['UPLOAD_FOLDER'] = './uploads' +app.config['METADATA_FILE'] = './uploads/metadata.txt' -# In-memory list to hold file metadata +# Initialize the files_data list with metadata on startup files_data = [] +@app.route('/uploads') +def list_files(): + """Display a list of uploaded files for download.""" + read_metadata() + sorted_files = sorted(files_data, key=lambda x: x.get('steps', 0), reverse=True) + return render_template('list_files.html', files=sorted_files) -@app.route('/metadata.txt') -def metadata(): - try: - return send_from_directory('./uploads', 'metadata.txt') - except FileNotFoundError: - return "metadata.txt not found.", 404 - -@app.route('/highest_steps') -def highest_steps(): - read_metadata() # Read metadata from file - - if not files_data: - return "No files uploaded.", 404 - - highest_steps_file = max(files_data, key=lambda x: x['steps']) - directory, filename = os.path.split(highest_steps_file['filepath']) - return send_from_directory(directory=directory, filename=filename) - -# Function to read metadata from file def read_metadata(): + """Read metadata from the metadata file.""" global files_data try: - with open('./uploads/metadata.txt', 'r') as f: + with open(app.config['METADATA_FILE'], 'r') as f: files_data = json.load(f) - except: + except FileNotFoundError: files_data = [] + except Exception as e: + print(f"Error reading metadata: {str(e)}") -# Function to write metadata to file def write_metadata(data): - with open('./uploads/metadata.txt', 'w') as f: + """Write metadata to the metadata file.""" + with open(app.config['METADATA_FILE'], 'w') as f: json.dump(data, f) # Read metadata from file on startup @@ -51,27 +41,30 @@ def write_metadata(data): @app.route('/') def index(): - read_metadata() # Read metadata from file - sorted_files = sorted(files_data, key=lambda x: x['steps'], reverse=True) + """Display a list of uploaded files with metadata sorted by steps.""" + read_metadata() + sorted_files = sorted(files_data, key=lambda x: x.get('steps', 0), reverse=True) return render_template('index.html', files=sorted_files) -# ... - @app.route('/uploads/') def download_file(filename): + """Download an uploaded file by providing the filename.""" return send_from_directory(app.config['UPLOAD_FOLDER'], filename) @app.route('/upload', methods=['POST']) def upload_file(): + """Upload a file, extract metadata, and save it with metadata.""" global files_data uploaded_file = request.files['file'] + + # Generate a unique filename using timestamp and SHA1 hash timestamp = datetime.now().strftime("%Y%m%d%H%M%S") sha1 = hashlib.sha1(uploaded_file.read()).hexdigest()[:10] uploaded_file.seek(0) original_filename = secure_filename(uploaded_file.filename) - # Extract the 'steps' from the original filename + # Extract the 'steps' from the original filename using regex match = re.search(r'poke_(\d+)_steps\.zip', original_filename) if match: steps = int(match.group(1)) @@ -82,7 +75,7 @@ def upload_file(): filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename) # Check if a file with the same 'steps' value already exists - existing_entry = next((entry for entry in files_data if entry['steps'] == steps), None) + existing_entry = next((entry for entry in files_data if entry.get('steps') == steps), None) if existing_entry: # Update the existing entry @@ -99,11 +92,11 @@ def upload_file(): # Sort the metadata by 'steps' in reverse order files_data.sort(key=lambda x: x.get('steps', 0), reverse=True) + + # Write metadata to the metadata file write_metadata(files_data) return jsonify({'success': True}) - if __name__ == '__main__': app.run(debug=True) - From ba10ccf2addd46f1e92de853a42ad341351b6b19 Mon Sep 17 00:00:00 2001 From: techmore Date: Mon, 23 Oct 2023 16:59:57 -0400 Subject: [PATCH 06/12] Update index.html --- www/templates/index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/www/templates/index.html b/www/templates/index.html index 62314dd0a..ae22003da 100644 --- a/www/templates/index.html +++ b/www/templates/index.html @@ -3,12 +3,12 @@ File Uploads - +

AI Plays Pokemon

- + + +
+ """ + + # Update the image sources based on the image names + for i, image_name in enumerate(image_names, start=1): + image_src = os.path.join(newest_session, image_name) + html_content += f' Image {i}\n' + + html_content += """
+ + + + """ + # Save the updated HTML content to a file + with open(output_file, 'w') as file: + file.write(html_content) + + print(f"HTML content updated and saved as '{output_file}'.") + if __name__ == '__main__': selected_checkpoint = None selected_checkpoint = show_menu(selected_checkpoint) From 6256a1b17eb71d1aa78f42eacab3a15c65ad1fb0 Mon Sep 17 00:00:00 2001 From: techmore Date: Sat, 28 Oct 2023 00:28:12 -0400 Subject: [PATCH 12/12] Update menu.py --- baselines/menu.py | 508 ++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 444 insertions(+), 64 deletions(-) diff --git a/baselines/menu.py b/baselines/menu.py index 720e5cdf8..558f60290 100644 --- a/baselines/menu.py +++ b/baselines/menu.py @@ -7,61 +7,147 @@ import glob from pathlib import Path import uuid -from red_gym_env import RedGymEnv -from stable_baselines3 import PPO -from stable_baselines3.common.vec_env import DummyVecEnv, SubprocVecEnv -from stable_baselines3.common.utils import set_random_seed -from stable_baselines3.common.callbacks import CheckpointCallback -import webbrowser +import datetime +import time +import sys # Make sure to import the sys module +import gzip +import pandas as pd # Add this import statement DEFAULT_BASE_URL = "http://127.0.0.1:5000" directory_path = 'downloaded_checkpoints' +# Add arguments for metrics and params +# upload and download included tensordata +# index.html session_xxxx instead of current folder to match the tensor setup +# for URL accept --refresh to set rate +# integrate hosting server in this location? then into script +# Hosting and download to downloaded checkpoints script integration +# Print out a location html that show the furthest going instance. + +# Newest Menu.py updates +# menu.py --info +# added git update detection +# Checkpoint monitoring scans the newest session for newly created zips and reports them with a timestamp +# Allow --URL for a custom external server +# match new imports from source +# Review need for HTML view file due to new patch + # it’s possible tensorboard does not allow “watching” +# index.html index information about json and other info +# Show highest value of the trained models in index.html + + +# Possible style upgrades +# consider If statement for Local and Downloaded sessions. Don’t display if nothing is found. + +#Line 155 run_script = f"python3 {selected_run}" used to run scripts in the folder + + +from watchdog.observers import Observer +from watchdog.events import FileSystemEventHandler + +def monitor_zip_files(directory_to_watch): + last_checkpoint_time = 0 + def handle_new_zip(file_path): + nonlocal last_checkpoint_time + current_time = time.time() + relative_path = os.path.relpath(file_path, start=directory_to_watch) + #if current_time - last_checkpoint_time >= print_interval: + interval = current_time - last_checkpoint_time + minutes, seconds = divmod(interval, 60) + print(f"Checkpoint {relative_path} created at {time.ctime()} interval : {int(minutes)} min {int(seconds)} sec") + last_checkpoint_time = current_time # Update the last checkpoint time + + # Watchdog event handler + class NewFileHandler(FileSystemEventHandler): + def on_created(self, event): + if not event.is_directory and event.src_path.endswith(".zip"): + handle_new_zip(event.src_path) + + # Start the watchdog observer to monitor the directory + observer = Observer() + event_handler = NewFileHandler() + observer.schedule(event_handler, path=directory_to_watch, recursive=False) + observer.start() + + print(f"\nMonitoring {directory_to_watch} for new checkpoints.") + try: + while True: + time.sleep(1) + except KeyboardInterrupt: + observer.stop() + observer.join() + + if not os.path.exists(directory_path): os.makedirs(directory_path) -def make_env(rank, env_conf, seed=0): - def _init(): - env = RedGymEnv(env_conf) - env.reset(seed=(seed + rank)) - return env - set_random_seed(seed) - return _init - def parse_args(): parser = argparse.ArgumentParser() - parser.add_argument('--menu') - parser.add_argument('--restore') - parser.add_argument('--upload') parser.add_argument('--url') + #parser.add_argument('--upload') + #parser.add_argument('--restore') + parser.add_argument('--view') + parser.add_argument('--info', action='store_true', help='Display help information') + + args = parser.parse_args() + + if args.url: + DEFAULT_BASE_URL = args.url + # does not error out with a faulty IP + #elif args.restore: + #List a checkpount menu without a file specified + # no runs should be lists + #selected_checkpoint = remote_actions() + #elif args.upload: + # if no file is offered you should list a menu + # #no runs should be listed + #upload(selection, session_dict) + elif args.view: + simple_create_index('index.html') + sys.exit() + elif args.info: + info() + sys.exit() + return parser.parse_args() def show_menu(selected_checkpoint): + update_optional = 0 + # Loop the menu indefinately while True: + # Check if your branch is up to date with 'origin/master' + up_to_date = is_up_to_date() + print("Your PokemonRedExperiments installation is up to date!" if up_to_date else "Your branch is not up to date with 'origin/master'.") + session_dict, downloaded_checkpoints = list_all_sessions_and_pokes() if not session_dict: print("No checkpoints found.") return selected_checkpoint - downloaded_checkpoint_count = len(session_dict) print(f"\nAvailable sessions sorted by their largest checkpoints:") for i, (session, largest_step) in enumerate(session_dict.items()): print(f" {i + 1}. {session}/poke-{largest_step}_steps.zip") - - print("\nDownloaded checkpoints:") + print("\n Downloaded checkpoints:") for i, checkpoint in enumerate(downloaded_checkpoints, start=downloaded_checkpoint_count + 1): print(f" {i}. {checkpoint}") - - print("\nDefault Runs:") + print("\n Default Runs:") matching_files = [file for file in os.listdir(os.getcwd()) if file.startswith("run_") and file.endswith(".py")] for i, file in enumerate(matching_files, start=downloaded_checkpoint_count + 1): print(f" {i}. {file}") - - print("\n 97. Resume from remote") - print(" 98. Upload to remote") + print("\n 95. Resume from remote") + print(" 96. Upload to remote") + print(" 97. Load a custom interactive checkpoint.") + print(" 98. Checkpoint creation live monitor.") print(" 99. View progress using index.html") + print(" 999. View map progress map.html") + #print(" 999. View progress using Tailwind index.html") + if update_optional == 1: + print(f" \n9999. Sync with code base update.") + subprocess.run(["git", "merge", "origin/master"]) + print(f"Your branch has been synced with 'origin/master'.\n") menu_selection = input("Enter the number of the menu option: ") + # Menu Logic if menu_selection.isdigit(): selection = int(menu_selection) if 1 <= selection <= len(session_dict): @@ -76,18 +162,35 @@ def show_menu(selected_checkpoint): selected_run = matching_files[selection - downloaded_checkpoint_count - len(downloaded_checkpoints) - 1] run_script = f"python3 {selected_run}" subprocess.run(run_script, shell=True) - elif menu_selection == '97': + elif menu_selection == '95': selected_checkpoint = remote_actions() if selected_checkpoint: - return selected_checkpoint - elif menu_selection == '98': + return selected_checkpoint + elif menu_selection == '96': selection = int(input("Enter your selection for remote upload: ")) upload(selection, session_dict) + elif menu_selection == '97': + #custom restore + return + elif menu_selection == '98': + all_folders = os.listdir() + session_folders = [folder for folder in all_folders if re.match(r'session_[0-9a-fA-F]{8}', folder)] + def get_creation_time(folder): + return os.path.getctime(folder) + session_folders.sort(key=get_creation_time, reverse=True) + if session_folders: + newest_session = session_folders[0] + monitor_zip_files(newest_session) elif menu_selection == '99': - create_index('index.html') - print('Open index.html to monitor the newest run.') - - + #create_index('index.html') + simple_create_index('index.html') + elif menu_selection == '999': + create_map('map.html') + #elif menu_selection == '999': + # tailwind_create_index('index.html') + elif menu_selection == '9999': + subprocess.run(["git", "pull", "origin/master"]) + print("Your branch has been synced with 'origin/master'.") else: print("Invalid selection.") else: @@ -98,7 +201,6 @@ def list_all_sessions_and_pokes(): session_folders = [folder for folder in all_folders if re.match(r'session_[0-9a-fA-F]{8}', folder)] session_dict = {} downloaded_checkpoints = [] - for session_folder in session_folders: poke_files = glob.glob(f"{session_folder}/poke_*_steps.zip") if poke_files: @@ -125,11 +227,9 @@ def remote_actions(): except json.decoder.JSONDecodeError as e: print("Error decoding JSON:", str(e)) return None - print(f"\nAvailable remote checkpoints:") for i, entry in enumerate(server_metadata): print(f"{i + 1}. Filename: {entry['filename']}, Steps: {entry['steps']}") - server_selection = input("Enter the number of the checkpoint you want to download: ") try: server_selection = int(server_selection) @@ -137,7 +237,6 @@ def remote_actions(): selected_server_entry = server_metadata[server_selection - 1] filename = selected_server_entry['filename'] download_response = requests.get(f"{BASE_URL}/uploads/{filename}") - if download_response.status_code == 200: with open(f"downloaded_checkpoints/{filename}", 'wb') as f: f.write(download_response.content) @@ -152,7 +251,6 @@ def remote_actions(): def restore(url, download_selection): response = requests.get(url) - if response.status_code == 200: filename = url.split("/")[-1] with open(filename, 'wb') as file: @@ -173,9 +271,26 @@ def upload(selection, session_dict): except (ValueError, IndexError): print("Invalid selection") +def make_env(rank, env_conf, seed=0): + def _init(): + env = RedGymEnv(env_conf) + env.reset(seed=(seed + rank)) + return env + set_random_seed(seed) + return _init + def main(selected_checkpoint): - sess_path = Path(f'session_{str(uuid.uuid4())[:8]}') + from red_gym_env import RedGymEnv + from stable_baselines3 import PPO + from stable_baselines3.common.vec_env import DummyVecEnv, SubprocVecEnv + from stable_baselines3.common.utils import set_random_seed + from stable_baselines3.common.callbacks import CheckpointCallback, CallbackList + from tensorboard_callback import TensorboardCallback + + use_wandb_logging = False ep_length = 2048 * 10 + sess_id = str(uuid.uuid4())[:8] + sess_path = Path(f'session_{sess_id}') env_config = { 'headless': True, 'save_final_state': True, 'early_stop': False, 'action_freq': 24, 'init_state': '../has_pokedex_nballs.state', 'max_steps': ep_length, @@ -185,11 +300,25 @@ def main(selected_checkpoint): 'explore_weight': 3 } print(env_config) - num_cpu = 16 + num_cpu = 16 # Also sets the number of episodes per training iteration env = SubprocVecEnv([make_env(i, env_config) for i in range(num_cpu)]) - checkpoint_callback = CheckpointCallback(save_freq=ep_length, save_path=sess_path, name_prefix='poke') + checkpoint_callback = CheckpointCallback(save_freq=ep_length, save_path=sess_path, + name_prefix='poke') + callbacks = [checkpoint_callback, TensorboardCallback()] + if use_wandb_logging: + import wandb + from wandb.integration.sb3 import WandbCallback + run = wandb.init( + project="pokemon-train", + id=sess_id, + config=env_config, + sync_tensorboard=True, + monitor_gym=True, + save_code=True, + ) + callbacks.append(WandbCallback()) learn_steps = 40 - + print('\nLoading checkpoint', selected_checkpoint, ' ... \n') model = PPO.load(selected_checkpoint, env=env) model.n_steps = ep_length @@ -198,62 +327,313 @@ def main(selected_checkpoint): model.rollout_buffer.n_envs = num_cpu model.rollout_buffer.reset() for i in range(learn_steps): - model.learn(total_timesteps=(ep_length) * num_cpu * 1000, callback=checkpoint_callback) + model = PPO('CnnPolicy', env, verbose=1, n_steps=ep_length // 8, batch_size=128, n_epochs=3, gamma=0.998, tensorboard_log=sess_path) + for i in range(learn_steps): + model.learn(total_timesteps=(ep_length)*num_cpu*1000, callback=CallbackList(callbacks)) + if use_wandb_logging: + run.finish() + +def info(): + help_message = """ +=============================================================================== + Pokemon Red Experiments - Help Menu +=============================================================================== + +Welcome to the Pokémon Plays AI script. This menu provides you with helpful +information about available options and actions. Use the following options: + +--url: Specify a custom external server URL for remote interactions. +--view --refresh NUM: View progress using the default HTML interface. +--info: Display this help menu. + +Additional Actions: + - '96' to resume from a remote checkpoint + allowing downloading from the Flask app.py in /www. + - '97' to upload your checkpoint to the server + enabling easy sharing of your checkpoints with yourself or others. + - '98' to monitor checkpoint creation live, allowing you to see when + a checkpoint is created so you can exit, minimizing loss. + - '99' to view progress creates an index.html file in the running session + folder that updates to display your JPEGs for the current run. + - '999' to view progress using Tailwind CSS-enhanced HTML. (Work in Progress) +To keep your script up to date with the code base, enter '9999'. If the script +detects you are out of date, it will append this option, but it is always available. -def create_index(output_file='index.html'): +Enjoy using Pokémon Plays AI! https://github.com/PWhiddy/PokemonRedExperiments + +=============================================================================== +""" + print(help_message) + +def is_up_to_date(): + return subprocess.run(["git", "pull", "origin"]) or subprocess.run(["git", "rev-parse", "HEAD"], capture_output=True, text=True).stdout.strip() == subprocess.run(["git", "rev-parse", "origin/master"], capture_output=True, text=True).stdout.strip() +import os +import pandas as pd +import gzip + +def create_map(output_file='map.html'): # Find all session folders within the current working directory session_folders = [folder for folder in os.listdir() if folder.startswith('session_')] if not session_folders: print("No 'session_' folders found in the current working directory.") return - # Sort the session folders by their names (timestamps) and get the newest one + # Create the initial HTML content + html_content = f""" + + + + + Map Locations + + + +

Map Locations and Counts

+ """ + + # Loop through session folders newest_session = max(session_folders, key=lambda folder: os.path.getctime(folder)) + # Create an empty dictionary to store unique locations and their counts for this session + unique_location_counts = {} + + # Loop through files in the session directory + image_dir = os.path.join(newest_session) + for filename in os.listdir(image_dir): + if filename.endswith('.csv.gz'): + print(f"Found CSV file: {filename}") + with gzip.open(os.path.join(newest_session, filename), 'rt') as file: + df = pd.read_csv(file) + if 'map_location' in df.columns: + total_lines = len(df) + unique_locations = df['map_location'].unique() + for location in unique_locations: + count = len(df[df['map_location'] == location]) + percentage = (count / total_lines) * 100 + unique_location_counts[location] = (count, percentage) + else: + print(f"'map_location' column not found in file: {filename}") + + # Add the unique location counts to the HTML content for this session + html_content += f""" +
+

{newest_session}

+ + """ + + for location, (count, percentage) in unique_location_counts.items(): + html_content += f""" + + + + + + + """ + + html_content += "
Loc: {location}Count: {count}Percentage: {percentage:.2f}%
" + # Complete the HTML content and save it to the file + html_content += """""" + session_html_file = os.path.join(newest_session, output_file) + with open(session_html_file, 'w') as file: + file.write(html_content) + + print(f"You can now open '{newest_session}/{output_file}' in the session directory to view your results.") + + +def simple_create_index(output_file='index.html', max_items=20): + # Find all session folders within the current working directory + pair_count = 0 + + session_folders = [folder for folder in os.listdir() if folder.startswith('session_')] + if not session_folders: + print("No 'session_' folders found in the current working directory.") + return + + # Sort the session folders by their names (timestamps) and get the newest one + newest_session = max(session_folders, key=lambda folder: os.path.getctime(folder)) image_names = [] + json_names = {} + zip_names = [] + imageid_badges = {} # Initialize a dictionary to store image_id and highest badge + imageid_values = {} # Initialize a dictionary to store image_id and values - # Get a list of image names in the newest session folder + # Get a list of file names in the newest session folder based on patterns image_dir = os.path.join(newest_session) for filename in os.listdir(image_dir): if filename.endswith('.jpeg'): image_names.append(filename) + elif filename.startswith('poke_') and filename.endswith('_steps.zip'): + zip_names.append(filename) + elif filename.startswith('all_runs_') and filename.endswith('.json'): + highest_badge_value = 0 # Initialize to 0 initially + image_id = filename.split('_')[2].split('.')[0] + json_names[image_id] = filename + # print(newest_session, "/", json_names[image_id]) + filepath = os.path.join(newest_session, json_names[image_id]) + # Open and parse the JSON data from the file + with open(filepath, 'r') as json_file: + data = json.load(json_file) + for entry in data: + badge_value = entry.get('badge', 0) # Default to 0 if 'badge' key doesn't exist + if badge_value > highest_badge_value: + highest_badge_value = badge_value + imageid_badges[image_id] = badge_value + print("new high value :", badge_value) + # Extract and store all the values from the JSON for this image + image_values = { + 'eve': round(entry.get('event', 0), 2), + 'lev': round(entry.get('level', 0), 2), + 'hea': round(entry.get('heal', 0), 2), + 'op_': round(entry.get('op_lvl', 0.0), 2), + 'dea': round(entry.get('dead', 0.0), 2), + 'bad': round(entry.get('badge', 0), 2), + 'exp': round(entry.get('explore', 0), 2) + } + imageid_values[image_id] = image_values + #elif filename.endswith('.csv.gz'): + # print(f"Found CSV file: {filename}") + # with gzip.open(os.path.join(newest_session, filename), 'rt') as file: + # df = pd.read_csv(file) + # if 'map_location' in df.columns: + # unique_locations = df['map_location'].unique() + # for location in unique_locations: + # count = len(df[df['map_location'] == location]) + # print(f"Location: {location}, Count: {count}") + # else: + # print(f"'map_location' column not found in file: {filename}") + + # Sort the ZIP files by the highest step value + zip_names.sort(key=lambda zip_name: int(zip_name.split('_')[1].split('_')[0]), reverse=True) + + # Get the creation timestamp of the newest session folder + session_creation_time = datetime.datetime.fromtimestamp(os.path.getctime(newest_session)).strftime('%Y-%m-%d %H:%M:%S') - # Create the updated HTML content with the image names + # Find the timestamp of the oldest image file + oldest_file_time = min([os.path.getctime(os.path.join(image_dir, f)) for f in os.listdir(image_dir) if f.endswith('.jpeg')]) + first_detected_checkpoint_time = datetime.datetime.fromtimestamp(oldest_file_time).strftime('%Y-%m-%d %H:%M:%S') + + # Calculate the time since the first detected checkpoint + current_time = datetime.datetime.now() + time_since_first_checkpoint = current_time - datetime.datetime.fromtimestamp(oldest_file_time) + time_since_first_checkpoint_str = str(time_since_first_checkpoint) + + # Create the updated HTML content with a softer red banner html_content = f""" - Dynamic Photo Grid + Pokemon Plays AI + - + - -
+ +
+
+

Pokemon Red Experiments

+

First Detected Checkpoint: {first_detected_checkpoint_time}

+

Current Timestamp: {current_time.strftime('%Y-%m-%d %H:%M:%S')}

+

Time Since First Detected Checkpoint: {time_since_first_checkpoint_str}

+ """ + if zip_names: + html_content += f'Download checkpoint ({zip_names[0]})' + # + html_content += f""" +
+
+ """ - - # Update the image sources based on the image names + + # Calculate the number of columns in the grid based on the number of images + num_cols = min(len(image_names), 4) + html_content += '' + # Display image names and JSON download buttons in a grid layout for i, image_name in enumerate(image_names, start=1): - image_src = os.path.join(newest_session, image_name) - html_content += f' Image {i}\n' - - html_content += """ - + if i > max_items: + break # Limit the number of displayed items + + image_id = image_name.split('_')[1].split('.')[0] + if i % num_cols == 1: + html_content += f' \n' + html_content += f' ' + html_content += """
' + html_content += f'Image {i}
' + html_content += f'{image_name}
' + # Add a button to download the JSON file + # Check if this image_id has extracted values + if image_id in json_names: + json_name = json_names[image_id] + #html_content += f'' + html_content += f'{json_name}' + + # Check if this image_id has extracted values + html_content += '' + if image_id in imageid_values: + values = imageid_values[image_id] + # Display the truncated keys and values in two columns + pairs = [f'{key}: {value}' for key, value in values.items()] + for i in range(0, len(pairs), 2): + pair1 = pairs[i] + pair2 = pairs[i + 1] if i + 1 < len(pairs) else '' # Handle odd number of pairs + html_content += '' # Use Tailwind classes to set margin and padding to 0 for rows + html_content += f'' + html_content += f'' + html_content += '
{pair1}{pair2}

' + #html_content += f'🏆' + + html_content += '
+
+
""" + + session_html_file = os.path.join(newest_session, output_file) + # Save the updated HTML content to a file - with open(output_file, 'w') as file: + with open(session_html_file, 'w') as file: file.write(html_content) - - print(f"HTML content updated and saved as '{output_file}'.") + print(f"You can now open '{newest_session}/{output_file}' in the session directory to view your results.") if __name__ == '__main__': + parse_args() selected_checkpoint = None selected_checkpoint = show_menu(selected_checkpoint) main(selected_checkpoint)