From 70a659f46810a84cd728749266fa8be12ed08af5 Mon Sep 17 00:00:00 2001 From: saji Date: Sun, 15 Dec 2024 13:54:23 -0600 Subject: [PATCH] simv2 --- README.md | 10 ++ notebooks/testing.ipynb | 65 +++++++++ notebooks/v1gym.ipynb | 247 ++++++++++++++++++++++++++++++++++ notebooks/v1sim.ipynb | 4 +- pdm.lock | 263 +++++++++++++++++++++++-------------- pyproject.toml | 2 +- report/report.tex | 136 +++++++++++++++++++ src/solarcarsim/physsim.py | 43 +----- src/solarcarsim/simv1.py | 37 +++--- src/solarcarsim/simv2.py | 192 +++++++++++++++++++++++++-- 10 files changed, 835 insertions(+), 164 deletions(-) create mode 100644 notebooks/testing.ipynb create mode 100644 notebooks/v1gym.ipynb create mode 100644 report/report.tex diff --git a/README.md b/README.md index 1f5ea90..4bb7924 100644 --- a/README.md +++ b/README.md @@ -1 +1,11 @@ # solarcarsim + + + +TODO: +fix wind (velocity + wind for drag) +make more functional +cleanup sim code +parameterize the environment +vectorize +cleanrl jax td3 \ No newline at end of file diff --git a/notebooks/testing.ipynb b/notebooks/testing.ipynb new file mode 100644 index 0000000..ef441e7 --- /dev/null +++ b/notebooks/testing.ipynb @@ -0,0 +1,65 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Array([ 0.0000000e+00, -2.5544850e-07, -1.4012958e-06, ...,\n", + " -1.1142221e-02, -1.1067827e-02, -1.1001030e-02], dtype=float32)" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import jax\n", + "import jax.numpy as jnp\n", + "from solarcarsim.physsim import CarParams, fractal_noise_1d\n", + "\n", + "\n", + "key = jax.random.key(0)\n", + "\n", + "slope = fractal_noise_1d(key, 10000, scale=1200, height_scale=0.08)\n", + "\n", + "slope" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# get an array of positions\n", + "positions = jnp.array([1.1,2.2,3.3,5,200.0], dtype=jnp.float32)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "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.12.7" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/notebooks/v1gym.ipynb b/notebooks/v1gym.ipynb new file mode 100644 index 0000000..fb0c02d --- /dev/null +++ b/notebooks/v1gym.ipynb @@ -0,0 +1,247 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import gymnasium as gym\n", + "from gymnasium.wrappers.jax_to_numpy import JaxToNumpy\n", + "from gymnasium.wrappers.vector import JaxToNumpy as VJaxToNumpy\n", + "from solarcarsim.simv1 import SolarRaceV1\n", + "from stable_baselines3.common.env_checker import check_env\n", + "from gymnasium.utils.env_checker import check_env as gym_check_env\n", + "env = SolarRaceV1()\n", + "wrapped_env = JaxToNumpy(env)" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/saji/Documents/Code/solarcarsim/.venv/lib/python3.12/site-packages/stable_baselines3/common/env_checker.py:271: UserWarning: Your observation wind has an unconventional shape (neither an image, nor a 1D vector). We recommend you to flatten the observation to have only a 1D vector or use a custom policy to properly process the data.\n", + " warnings.warn(\n", + "/home/saji/Documents/Code/solarcarsim/.venv/lib/python3.12/site-packages/gymnasium/utils/env_checker.py:384: UserWarning: \u001b[33mWARN: The environment (>) is different from the unwrapped version (). This could effect the environment checker as the environment most likely has a wrapper applied to it. We recommend using the raw environment for `check_env` using `env.unwrapped`.\u001b[0m\n", + " logger.warn(\n", + "/home/saji/Documents/Code/solarcarsim/.venv/lib/python3.12/site-packages/gymnasium/utils/env_checker.py:434: UserWarning: \u001b[33mWARN: Not able to test alternative render modes due to the environment not having a spec. Try instantiating the environment through `gymnasium.make`\u001b[0m\n", + " logger.warn(\n" + ] + } + ], + "source": [ + "env.reset()\n", + "check_env(wrapped_env)\n", + "gym_check_env(wrapped_env)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/saji/Documents/Code/solarcarsim/.venv/lib/python3.12/site-packages/stable_baselines3/common/buffers.py:605: UserWarning: This system does not have apparently enough memory to store the complete replay buffer 80.85GB > 53.66GB\n", + " warnings.warn(\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Using cuda device\n", + "Wrapping the env with a `Monitor` wrapper\n", + "Wrapping the env in a DummyVecEnv.\n" + ] + }, + { + "ename": "KeyboardInterrupt", + "evalue": "", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mKeyboardInterrupt\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[3], line 4\u001b[0m\n\u001b[1;32m 2\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mstable_baselines3\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m TD3\n\u001b[1;32m 3\u001b[0m model \u001b[38;5;241m=\u001b[39m TD3(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mMultiInputPolicy\u001b[39m\u001b[38;5;124m\"\u001b[39m, wrapped_env, verbose\u001b[38;5;241m=\u001b[39m\u001b[38;5;241m1\u001b[39m)\n\u001b[0;32m----> 4\u001b[0m \u001b[43mmodel\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mlearn\u001b[49m\u001b[43m(\u001b[49m\u001b[43mtotal_timesteps\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;241;43m30_000\u001b[39;49m\u001b[43m)\u001b[49m\n", + "File \u001b[0;32m~/Documents/Code/solarcarsim/.venv/lib/python3.12/site-packages/stable_baselines3/td3/td3.py:222\u001b[0m, in \u001b[0;36mTD3.learn\u001b[0;34m(self, total_timesteps, callback, log_interval, tb_log_name, reset_num_timesteps, progress_bar)\u001b[0m\n\u001b[1;32m 213\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mlearn\u001b[39m(\n\u001b[1;32m 214\u001b[0m \u001b[38;5;28mself\u001b[39m: SelfTD3,\n\u001b[1;32m 215\u001b[0m total_timesteps: \u001b[38;5;28mint\u001b[39m,\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 220\u001b[0m progress_bar: \u001b[38;5;28mbool\u001b[39m \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mFalse\u001b[39;00m,\n\u001b[1;32m 221\u001b[0m ) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m SelfTD3:\n\u001b[0;32m--> 222\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[43mlearn\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 223\u001b[0m \u001b[43m \u001b[49m\u001b[43mtotal_timesteps\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mtotal_timesteps\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 224\u001b[0m \u001b[43m \u001b[49m\u001b[43mcallback\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mcallback\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 225\u001b[0m \u001b[43m \u001b[49m\u001b[43mlog_interval\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mlog_interval\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 226\u001b[0m \u001b[43m \u001b[49m\u001b[43mtb_log_name\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mtb_log_name\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 227\u001b[0m \u001b[43m \u001b[49m\u001b[43mreset_num_timesteps\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mreset_num_timesteps\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 228\u001b[0m \u001b[43m \u001b[49m\u001b[43mprogress_bar\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mprogress_bar\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 229\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[0;32m~/Documents/Code/solarcarsim/.venv/lib/python3.12/site-packages/stable_baselines3/common/off_policy_algorithm.py:347\u001b[0m, in \u001b[0;36mOffPolicyAlgorithm.learn\u001b[0;34m(self, total_timesteps, callback, log_interval, tb_log_name, reset_num_timesteps, progress_bar)\u001b[0m\n\u001b[1;32m 345\u001b[0m \u001b[38;5;66;03m# Special case when the user passes `gradient_steps=0`\u001b[39;00m\n\u001b[1;32m 346\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m gradient_steps \u001b[38;5;241m>\u001b[39m \u001b[38;5;241m0\u001b[39m:\n\u001b[0;32m--> 347\u001b[0m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mtrain\u001b[49m\u001b[43m(\u001b[49m\u001b[43mbatch_size\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mbatch_size\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mgradient_steps\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mgradient_steps\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 349\u001b[0m callback\u001b[38;5;241m.\u001b[39mon_training_end()\n\u001b[1;32m 351\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m\n", + "File \u001b[0;32m~/Documents/Code/solarcarsim/.venv/lib/python3.12/site-packages/stable_baselines3/td3/td3.py:184\u001b[0m, in \u001b[0;36mTD3.train\u001b[0;34m(self, gradient_steps, batch_size)\u001b[0m\n\u001b[1;32m 182\u001b[0m critic_loss \u001b[38;5;241m=\u001b[39m \u001b[38;5;28msum\u001b[39m(F\u001b[38;5;241m.\u001b[39mmse_loss(current_q, target_q_values) \u001b[38;5;28;01mfor\u001b[39;00m current_q \u001b[38;5;129;01min\u001b[39;00m current_q_values)\n\u001b[1;32m 183\u001b[0m \u001b[38;5;28;01massert\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(critic_loss, th\u001b[38;5;241m.\u001b[39mTensor)\n\u001b[0;32m--> 184\u001b[0m critic_losses\u001b[38;5;241m.\u001b[39mappend(\u001b[43mcritic_loss\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mitem\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m)\n\u001b[1;32m 186\u001b[0m \u001b[38;5;66;03m# Optimize the critics\u001b[39;00m\n\u001b[1;32m 187\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mcritic\u001b[38;5;241m.\u001b[39moptimizer\u001b[38;5;241m.\u001b[39mzero_grad()\n", + "\u001b[0;31mKeyboardInterrupt\u001b[0m: " + ] + } + ], + "source": [ + "# import a model and try it out!\n", + "from sbx import TD3\n", + "model = TD3(\"MultiInputPolicy\", env, verbose=1)\n", + "model.learn(total_timesteps=30_000)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "vec_env = model.get_env()\n", + "import matplotlib.pyplot as plt\n", + "import jax.numpy as jnp\n", + "obs = vec_env.reset()\n", + "actions = []\n", + "obs_list = []\n", + "rewards = []\n", + "for i in range(1000):\n", + " action, _state = model.predict(obs, deterministic=True)\n", + " actions.append(action)\n", + " obs, reward, done, info = vec_env.step(action)\n", + " obs_list.append(obs)\n", + " rewards.append(reward)\n", + "\n", + " \n", + " # VecEnv resets automatically\n", + " if done:\n", + " break\n", + " # obs = vec_env.reset()\n", + "\n", + "position = jnp.array([x['position'] for x in obs_list]).flatten()\n", + "energy = jnp.array([x['energy'] for x in obs_list]).flatten()\n", + "actions = jnp.array(actions).flatten()" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[]" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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ys3NlSQNHDhQoaGheuONN1RWVqaJEycqKChIq1at0mOPPaa7777bc4zf/OY3SklJ0fDhw9XQ0KDnnntOWVlZevfdd7vyVAAAAAAA6BRdGtQXLVqk5cuXe/4eO3asJGnNmjWaPHmyAgIC9Oyzz2revHkyxmjgwIH69a9/rdtvv92zT1NTkxYsWKCioiKFhIRo1KhReu+993TppZd25akAAAAAANApujSoL1u27JRzqE+bNk3Tpk075THuvfde3XvvvWe5MmtyOBx68MEHT3nbP+Br9FN0B/RTdAf0U1gdfRTdQU/ppzZjTmdseAAAAAAA0BXsvi4AAAAAAAB8gaAOAAAAAICFENQBAAAAALAQgjoAAAAAABZCUAcAAAAAwEII6hb27LPPqn///goKClJ6ero+/vhjX5eEc9RDDz0km83mtaSlpXm2NzQ0aM6cOYqKilJoaKiuueYalZWV+bBinAvef/99ffe731ViYqJsNptee+01r+3GGC1atEgJCQkKDg5WZmam9uzZ49WmoqJCM2bMkNPpVEREhG677TbV1NR04Vmgp+uon958880nvL5+dapa+ik60+LFi3X++ecrLCxMsbGxuuqqq5SXl+fV5nTe5wsKCjR9+nSFhIQoNjZW99xzj1paWrryVNCDnU4/nTx58gmvp7NmzfJq0536KUHdol5++WXNnz9fDz74oD755BONHj1aU6dOVXl5ua9Lwzlq+PDhKikp8Szr16/3bJs3b57eeOMNvfLKK1q3bp2Ki4v1/e9/34fV4lxQW1ur0aNH69lnn213+xNPPKGnn35aS5cuVU5Ojnr16qWpU6eqoaHB02bGjBnasWOHVq1apTfffFPvv/++Zs6c2VWngHNAR/1UkqZNm+b1+vrSSy95baefojOtW7dOc+bM0YYNG7Rq1So1Nzfr8ssvV21tradNR+/zra2tmj59upqamvTRRx9p+fLlWrZsmRYtWuSLU0IPdDr9VJJuv/12r9fTJ554wrOt2/VTA0uaMGGCmTNnjufv1tZWk5iYaBYvXuzDqnCuevDBB83o0aPb3VZZWWkCAgLMK6+84lm3a9cuI8lkZ2d3UYU410kyr776qudvt9tt4uPjzZNPPulZV1lZaRwOh3nppZeMMcbs3LnTSDIbN270tHnnnXeMzWYzRUVFXVY7zh1f7afGGHPTTTeZK6+88qT70E/R1crLy40ks27dOmPM6b3Pv/3228Zut5vS0lJPmyVLlhin02kaGxu79gRwTvhqPzXGmEsuucTcddddJ92nu/VTrqhbUFNTkzZv3qzMzEzPOrvdrszMTGVnZ/uwMpzL9uzZo8TERKWmpmrGjBkqKCiQJG3evFnNzc1e/TUtLU19+/alv8Jn8vPzVVpa6tUvw8PDlZ6e7umX2dnZioiI0Pjx4z1tMjMzZbfblZOT0+U149y1du1axcbGasiQIZo9e7aOHj3q2UY/RVerqqqSJEVGRko6vff57OxsjRw5UnFxcZ42U6dOlcvl0o4dO7qwepwrvtpPj3vxxRcVHR2tESNGaOHChaqrq/Ns62791N/XBeBER44cUWtrq1cnkqS4uDjt3r3bR1XhXJaenq5ly5ZpyJAhKikp0cMPP6yLLrpI27dvV2lpqQIDAxUREeG1T1xcnEpLS31TMM55x/tee6+jx7eVlpYqNjbWa7u/v78iIyPpu+gy06ZN0/e//32lpKRo3759+tnPfqYrrrhC2dnZ8vPzo5+iS7ndbs2dO1eTJk3SiBEjJOm03udLS0vbfb09vg04m9rrp5L0wx/+UP369VNiYqK2bt2q++67T3l5efq///s/Sd2vnxLUAXToiiuu8Pw+atQopaenq1+/fvr73/+u4OBgH1YGAN3b9ddf7/l95MiRGjVqlAYMGKC1a9dqypQpPqwM56I5c+Zo+/btXuPQAFZzsn765bE7Ro4cqYSEBE2ZMkX79u3TgAEDurrMb4xb3y0oOjpafn5+J4ymWVZWpvj4eB9VBXwhIiJCgwcP1t69exUfH6+mpiZVVlZ6taG/wpeO971TvY7Gx8efMEBnS0uLKioq6LvwmdTUVEVHR2vv3r2S6KfoOnfccYfefPNNrVmzRklJSZ71p/M+Hx8f3+7r7fFtwNlysn7anvT0dEnyej3tTv2UoG5BgYGBGjdunFavXu1Z53a7tXr1amVkZPiwMqBNTU2N9u3bp4SEBI0bN04BAQFe/TUvL08FBQX0V/hMSkqK4uPjvfqly+VSTk6Op19mZGSosrJSmzdv9rTJysqS2+32vLkDXe3QoUM6evSoEhISJNFP0fmMMbrjjjv06quvKisrSykpKV7bT+d9PiMjQ9u2bfP6UmnVqlVyOp0aNmxY15wIerSO+ml7cnNzJcnr9bRb9VNfj2aH9q1YscI4HA6zbNkys3PnTjNz5kwTERHhNUoh0FUWLFhg1q5da/Lz882HH35oMjMzTXR0tCkvLzfGGDNr1izTt29fk5WVZTZt2mQyMjJMRkaGj6tGT1ddXW22bNlitmzZYiSZX//612bLli3m4MGDxhhjHn/8cRMREWFef/11s3XrVnPllVealJQUU19f7znGtGnTzNixY01OTo5Zv369GTRokLnhhht8dUrogU7VT6urq83dd99tsrOzTX5+vnnvvffMeeedZwYNGmQaGho8x6CfojPNnj3bhIeHm7Vr15qSkhLPUldX52nT0ft8S0uLGTFihLn88stNbm6uWblypYmJiTELFy70xSmhB+qon+7du9c88sgjZtOmTSY/P9+8/vrrJjU11Vx88cWeY3S3fkpQt7BnnnnG9O3b1wQGBpoJEyaYDRs2+LoknKOuu+46k5CQYAIDA02fPn3MddddZ/bu3evZXl9fb37yk5+Y3r17m5CQEHP11VebkpISH1aMc8GaNWuMpBOWm266yRjTNkXbAw88YOLi4ozD4TBTpkwxeXl5Xsc4evSoueGGG0xoaKhxOp3mlltuMdXV1T44G/RUp+qndXV15vLLLzcxMTEmICDA9OvXz9x+++0nfClPP0Vnaq9/SjIvvPCCp83pvM8fOHDAXHHFFSY4ONhER0ebBQsWmObm5i4+G/RUHfXTgoICc/HFF5vIyEjjcDjMwIEDzT333GOqqqq8jtOd+qnNGGO67vo9AAAAAAA4FZ5RBwAAAADAQgjqAAAAAABYCEEdAAAAAAALIagDAAAAAGAhBHUAAAAAACyEoA4AAAAAgIUQ1AEAAAAAsBCCOgAAAAAAFkJQBwAAAADAQgjqAAAAAABYCEEdAAAAAAAL+f/5DiE6L+PaHwAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "fig, (ax1, ax2, ax3) = plt.subplots(3,1, figsize=(12,6))\n", + "ax1.plot(position, label=\"position\")\n", + "ax2.plot(actions, label=\"energy\")\n", + "ax3.plot(rewards[0:250])\n", + "# plt.legend()" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Array([-1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1.,\n", + " -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1.,\n", + " -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1.,\n", + " -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1.,\n", + " -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1.,\n", + " -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1.,\n", + " -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1.,\n", + " -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1.,\n", + " -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1.,\n", + " -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1.,\n", + " -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1.,\n", + " -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1.,\n", + " -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1.,\n", + " -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1.,\n", + " -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1.,\n", + " -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1.,\n", + " -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1.,\n", + " -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1.,\n", + " -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1.,\n", + " -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1.,\n", + " -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1.,\n", + " -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1.,\n", + " -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1.,\n", + " -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1.,\n", + " -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1.,\n", + " -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1.,\n", + " -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1.,\n", + " -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1.,\n", + " -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1.,\n", + " -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1.,\n", + " -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1.,\n", + " -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1.,\n", + " -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1.,\n", + " -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1.,\n", + " -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1.,\n", + " -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1.,\n", + " -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1.,\n", + " -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1.,\n", + " -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1.,\n", + " -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1.,\n", + " -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1.,\n", + " -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1.,\n", + " -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1.,\n", + " -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1.,\n", + " -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1.,\n", + " -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1., -1.,\n", + " -1., -1., -1.], dtype=float32)" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "actions" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "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.12.7" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/notebooks/v1sim.ipynb b/notebooks/v1sim.ipynb index bb2d9fe..d076f90 100644 --- a/notebooks/v1sim.ipynb +++ b/notebooks/v1sim.ipynb @@ -564,7 +564,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "metadata": {}, "outputs": [], "source": [ @@ -576,7 +576,7 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 2, "metadata": {}, "outputs": [ { diff --git a/pdm.lock b/pdm.lock index cc7b888..011a0cf 100644 --- a/pdm.lock +++ b/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "dev"] strategy = ["inherit_metadata"] lock_version = "4.5.0" -content_hash = "sha256:81e26f71acf1a583b21280b235fa2ac16165ac824ae8483bd391b88406421aa4" +content_hash = "sha256:a3b65f863c554725c33d452fd759776141740661fa3555d306ed08563a7e16e2" [[metadata.targets]] requires_python = ">=3.12,<3.13" @@ -152,7 +152,7 @@ version = "0.4.6" requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" summary = "Cross-platform colored terminal text." groups = ["default", "dev"] -marker = "python_version >= \"3.12\" and python_version < \"3.13\" and sys_platform == \"win32\"" +marker = "sys_platform == \"win32\" and python_version >= \"3.12\" and python_version < \"3.13\" or platform_system == \"Windows\" and python_version >= \"3.12\" and python_version < \"3.13\"" files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, @@ -230,13 +230,30 @@ name = "decorator" version = "5.1.1" requires_python = ">=3.5" summary = "Decorators for Humans" -groups = ["dev"] +groups = ["default", "dev"] marker = "python_version >= \"3.12\" and python_version < \"3.13\"" 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 = "dm-tree" +version = "0.1.8" +summary = "Tree is a library for working with nested data structures." +groups = ["default"] +marker = "python_version >= \"3.12\" and python_version < \"3.13\"" +files = [ + {file = "dm-tree-0.1.8.tar.gz", hash = "sha256:0fcaabbb14e7980377439e7140bd05552739ca5e515ecb3119f234acee4b9430"}, + {file = "dm_tree-0.1.8-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:ea9e59e0451e7d29aece402d9f908f2e2a80922bcde2ebfd5dcb07750fcbfee8"}, + {file = "dm_tree-0.1.8-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:94d3f0826311f45ee19b75f5b48c99466e4218a0489e81c0f0167bda50cacf22"}, + {file = "dm_tree-0.1.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:435227cf3c5dc63f4de054cf3d00183790bd9ead4c3623138c74dde7f67f521b"}, + {file = "dm_tree-0.1.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09964470f76a5201aff2e8f9b26842976de7889300676f927930f6285e256760"}, + {file = "dm_tree-0.1.8-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:75c5d528bb992981c20793b6b453e91560784215dffb8a5440ba999753c14ceb"}, + {file = "dm_tree-0.1.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0a94aba18a35457a1b5cd716fd7b46c5dafdc4cf7869b4bae665b91c4682a8e"}, + {file = "dm_tree-0.1.8-cp312-cp312-win_amd64.whl", hash = "sha256:96a548a406a6fb15fe58f6a30a57ff2f2aafbf25f05afab00c8f5e5977b6c715"}, +] + [[package]] name = "etils" version = "1.11.0" @@ -379,6 +396,47 @@ files = [ {file = "fsspec-2024.10.0.tar.gz", hash = "sha256:eda2d8a4116d4f2429db8550f2457da57279247dd930bb12f821b58391359493"}, ] +[[package]] +name = "gast" +version = "0.6.0" +requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" +summary = "Python AST that abstracts the underlying Python version" +groups = ["default"] +marker = "python_version >= \"3.12\" and python_version < \"3.13\"" +files = [ + {file = "gast-0.6.0-py3-none-any.whl", hash = "sha256:52b182313f7330389f72b069ba00f174cfe2a06411099547288839c6cbafbd54"}, + {file = "gast-0.6.0.tar.gz", hash = "sha256:88fc5300d32c7ac6ca7b515310862f71e6fdf2c029bbec7c66c0f5dd47b6b1fb"}, +] + +[[package]] +name = "gym" +version = "0.26.2" +requires_python = ">=3.6" +summary = "Gym: A universal API for reinforcement learning environments" +groups = ["default"] +marker = "python_version >= \"3.12\" and python_version < \"3.13\"" +dependencies = [ + "cloudpickle>=1.2.0", + "dataclasses==0.8; python_version == \"3.6\"", + "gym-notices>=0.0.4", + "importlib-metadata>=4.8.0; python_version < \"3.10\"", + "numpy>=1.18.0", +] +files = [ + {file = "gym-0.26.2.tar.gz", hash = "sha256:e0d882f4b54f0c65f203104c24ab8a38b039f1289986803c7d02cdbe214fbcc4"}, +] + +[[package]] +name = "gym-notices" +version = "0.0.8" +summary = "Notices for gym" +groups = ["default"] +marker = "python_version >= \"3.12\" and python_version < \"3.13\"" +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 = "gymnasium" version = "1.0.0" @@ -417,6 +475,29 @@ files = [ {file = "gymnasium-1.0.0.tar.gz", hash = "sha256:9d2b66f30c1b34fe3c2ce7fae65ecf365d0e9982d2b3d860235e773328a3b403"}, ] +[[package]] +name = "gymnax" +version = "0.0.8" +requires_python = ">=3.10" +summary = "JAX-compatible version of Open AI's gym environments" +groups = ["default"] +marker = "python_version >= \"3.12\" and python_version < \"3.13\"" +dependencies = [ + "chex", + "flax", + "gym>=0.26", + "gymnasium", + "jax", + "jaxlib", + "matplotlib", + "pyyaml", + "seaborn", +] +files = [ + {file = "gymnax-0.0.8-py3-none-any.whl", hash = "sha256:0af7edde1b71d74be8007ffe1e6338f8ce66693b1b78ae479c0c0cd02b10de03"}, + {file = "gymnax-0.0.8.tar.gz", hash = "sha256:81defc17f52a30a84338b3daa574d7a3bb112f2656f45c783a71efe31eea68ff"}, +] + [[package]] name = "humanize" version = "4.11.0" @@ -541,76 +622,6 @@ files = [ {file = "jax-0.4.37.tar.gz", hash = "sha256:7774f3d9e23fe199c65589c680c5a5be87a183b89598421a632d8245222b637b"}, ] -[[package]] -name = "jax-cuda12-pjrt" -version = "0.4.36" -summary = "JAX XLA PJRT Plugin for NVIDIA GPUs" -groups = ["default"] -marker = "python_version >= \"3.12\" and python_version < \"3.13\"" -files = [ - {file = "jax_cuda12_pjrt-0.4.36-py3-none-manylinux2014_aarch64.whl", hash = "sha256:1dfc0bec0820ba801b61e9421064b6e58238c430b4ad8f54043323d93c0217c6"}, - {file = "jax_cuda12_pjrt-0.4.36-py3-none-manylinux2014_x86_64.whl", hash = "sha256:e3c3705d8db7d63da9abfaebf06f5cd0667f5acb0748a5c5eb00d80041e922ed"}, -] - -[[package]] -name = "jax-cuda12-plugin" -version = "0.4.36" -requires_python = ">=3.10" -summary = "JAX Plugin for NVIDIA GPUs" -groups = ["default"] -marker = "python_version >= \"3.12\" and python_version < \"3.13\"" -dependencies = [ - "jax-cuda12-pjrt==0.4.36", -] -files = [ - {file = "jax_cuda12_plugin-0.4.36-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:6a0b0c2bdc1da2eea2c20723a1e8f39b3cda67d24c665de936647e8091f5790d"}, - {file = "jax_cuda12_plugin-0.4.36-cp312-cp312-manylinux2014_x86_64.whl", hash = "sha256:5d4727fb519fedc06a9a984d5a0714804d81ef126a2cb60cefd5cbc4a3ea2627"}, -] - -[[package]] -name = "jax-cuda12-plugin" -version = "0.4.36" -extras = ["with_cuda"] -requires_python = ">=3.10" -summary = "JAX Plugin for NVIDIA GPUs" -groups = ["default"] -marker = "python_version >= \"3.12\" and python_version < \"3.13\"" -dependencies = [ - "jax-cuda12-plugin==0.4.36", - "nvidia-cublas-cu12>=12.1.3.1", - "nvidia-cuda-cupti-cu12>=12.1.105", - "nvidia-cuda-nvcc-cu12>=12.6.85", - "nvidia-cuda-runtime-cu12>=12.1.105", - "nvidia-cudnn-cu12<10.0,>=9.1", - "nvidia-cufft-cu12>=11.0.2.54", - "nvidia-cusolver-cu12>=11.4.5.107", - "nvidia-cusparse-cu12>=12.1.0.106", - "nvidia-nccl-cu12>=2.18.1", - "nvidia-nvjitlink-cu12>=12.1.105", -] -files = [ - {file = "jax_cuda12_plugin-0.4.36-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:6a0b0c2bdc1da2eea2c20723a1e8f39b3cda67d24c665de936647e8091f5790d"}, - {file = "jax_cuda12_plugin-0.4.36-cp312-cp312-manylinux2014_x86_64.whl", hash = "sha256:5d4727fb519fedc06a9a984d5a0714804d81ef126a2cb60cefd5cbc4a3ea2627"}, -] - -[[package]] -name = "jax" -version = "0.4.37" -extras = ["cuda12"] -requires_python = ">=3.10" -summary = "Differentiate, compile, and transform Numpy code." -groups = ["default"] -marker = "python_version >= \"3.12\" and python_version < \"3.13\"" -dependencies = [ - "jax-cuda12-plugin[with_cuda]<=0.4.37,>=0.4.36", - "jax==0.4.37", - "jaxlib==0.4.36", -] -files = [ - {file = "jax-0.4.37-py3-none-any.whl", hash = "sha256:bdc0686d7e5a944e2d38026eae632214d98dd2d91869cbcedbf1c11298ae3e3e"}, - {file = "jax-0.4.37.tar.gz", hash = "sha256:7774f3d9e23fe199c65589c680c5a5be87a183b89598421a632d8245222b637b"}, -] - [[package]] name = "jaxlib" version = "0.4.36" @@ -926,7 +937,7 @@ version = "12.4.5.8" requires_python = ">=3" summary = "CUBLAS native runtime libraries" groups = ["default"] -marker = "python_version >= \"3.12\" and python_version < \"3.13\"" +marker = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version >= \"3.12\" and python_version < \"3.13\"" files = [ {file = "nvidia_cublas_cu12-12.4.5.8-py3-none-manylinux2014_aarch64.whl", hash = "sha256:0f8aa1706812e00b9f19dfe0cdb3999b092ccb8ca168c0db5b8ea712456fd9b3"}, {file = "nvidia_cublas_cu12-12.4.5.8-py3-none-manylinux2014_x86_64.whl", hash = "sha256:2fc8da60df463fdefa81e323eef2e36489e1c94335b5358bcb38360adf75ac9b"}, @@ -939,26 +950,13 @@ version = "12.4.127" requires_python = ">=3" summary = "CUDA profiling tools runtime libs." groups = ["default"] -marker = "python_version >= \"3.12\" and python_version < \"3.13\"" +marker = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version >= \"3.12\" and python_version < \"3.13\"" files = [ {file = "nvidia_cuda_cupti_cu12-12.4.127-py3-none-manylinux2014_aarch64.whl", hash = "sha256:79279b35cf6f91da114182a5ce1864997fd52294a87a16179ce275773799458a"}, {file = "nvidia_cuda_cupti_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl", hash = "sha256:9dec60f5ac126f7bb551c055072b69d85392b13311fcc1bcda2202d172df30fb"}, {file = "nvidia_cuda_cupti_cu12-12.4.127-py3-none-win_amd64.whl", hash = "sha256:5688d203301ab051449a2b1cb6690fbe90d2b372f411521c86018b950f3d7922"}, ] -[[package]] -name = "nvidia-cuda-nvcc-cu12" -version = "12.6.85" -requires_python = ">=3" -summary = "CUDA nvcc" -groups = ["default"] -marker = "python_version >= \"3.12\" and python_version < \"3.13\"" -files = [ - {file = "nvidia_cuda_nvcc_cu12-12.6.85-py3-none-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d75d9d74599f4d7c0865df19ed21b739e6cb77a6497a3f73d6f61e8038a765e4"}, - {file = "nvidia_cuda_nvcc_cu12-12.6.85-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5d2edd5531b13e3daac8ffee9fc2b70a147e6088b2af2565924773d63d36d294"}, - {file = "nvidia_cuda_nvcc_cu12-12.6.85-py3-none-win_amd64.whl", hash = "sha256:aa04742337973dcb5bcccabb590edc8834c60ebfaf971847888d24ffef6c46b5"}, -] - [[package]] name = "nvidia-cuda-nvrtc-cu12" version = "12.4.127" @@ -978,7 +976,7 @@ version = "12.4.127" requires_python = ">=3" summary = "CUDA Runtime native Libraries" groups = ["default"] -marker = "python_version >= \"3.12\" and python_version < \"3.13\"" +marker = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version >= \"3.12\" and python_version < \"3.13\"" files = [ {file = "nvidia_cuda_runtime_cu12-12.4.127-py3-none-manylinux2014_aarch64.whl", hash = "sha256:961fe0e2e716a2a1d967aab7caee97512f71767f852f67432d572e36cb3a11f3"}, {file = "nvidia_cuda_runtime_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl", hash = "sha256:64403288fa2136ee8e467cdc9c9427e0434110899d07c779f25b5c068934faa5"}, @@ -991,7 +989,7 @@ version = "9.1.0.70" requires_python = ">=3" summary = "cuDNN runtime libraries" groups = ["default"] -marker = "python_version >= \"3.12\" and python_version < \"3.13\"" +marker = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version >= \"3.12\" and python_version < \"3.13\"" dependencies = [ "nvidia-cublas-cu12", ] @@ -1006,7 +1004,7 @@ version = "11.2.1.3" requires_python = ">=3" summary = "CUFFT native runtime libraries" groups = ["default"] -marker = "python_version >= \"3.12\" and python_version < \"3.13\"" +marker = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version >= \"3.12\" and python_version < \"3.13\"" dependencies = [ "nvidia-nvjitlink-cu12", ] @@ -1035,7 +1033,7 @@ version = "11.6.1.9" requires_python = ">=3" summary = "CUDA solver native runtime libraries" groups = ["default"] -marker = "python_version >= \"3.12\" and python_version < \"3.13\"" +marker = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version >= \"3.12\" and python_version < \"3.13\"" dependencies = [ "nvidia-cublas-cu12", "nvidia-cusparse-cu12", @@ -1053,7 +1051,7 @@ version = "12.3.1.170" requires_python = ">=3" summary = "CUSPARSE native runtime libraries" groups = ["default"] -marker = "python_version >= \"3.12\" and python_version < \"3.13\"" +marker = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version >= \"3.12\" and python_version < \"3.13\"" dependencies = [ "nvidia-nvjitlink-cu12", ] @@ -1069,7 +1067,7 @@ version = "2.21.5" requires_python = ">=3" summary = "NVIDIA Collective Communication Library (NCCL) Runtime" groups = ["default"] -marker = "python_version >= \"3.12\" and python_version < \"3.13\"" +marker = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version >= \"3.12\" and python_version < \"3.13\"" files = [ {file = "nvidia_nccl_cu12-2.21.5-py3-none-manylinux2014_x86_64.whl", hash = "sha256:8579076d30a8c24988834445f8d633c697d42397e92ffc3f63fa26766d25e0a0"}, ] @@ -1080,7 +1078,7 @@ version = "12.4.127" requires_python = ">=3" summary = "Nvidia JIT LTO Library" groups = ["default"] -marker = "python_version >= \"3.12\" and python_version < \"3.13\"" +marker = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version >= \"3.12\" and python_version < \"3.13\"" files = [ {file = "nvidia_nvjitlink_cu12-12.4.127-py3-none-manylinux2014_aarch64.whl", hash = "sha256:4abe7fef64914ccfa909bc2ba39739670ecc9e820c83ccc7a6ed414122599b83"}, {file = "nvidia_nvjitlink_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl", hash = "sha256:06b3b9b25bf3f8af351d664978ca26a16d2c5127dbd53c0497e28d1fb9611d57"}, @@ -1653,6 +1651,29 @@ files = [ {file = "rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098"}, ] +[[package]] +name = "sbx-rl" +version = "0.18.0" +requires_python = ">=3.8" +summary = "Jax version of Stable Baselines, implementations of reinforcement learning algorithms." +groups = ["default"] +marker = "python_version >= \"3.12\" and python_version < \"3.13\"" +dependencies = [ + "flax", + "jax", + "jaxlib", + "optax; python_version >= \"3.9.0\"", + "optax<0.1.8; python_version < \"3.9.0\"", + "rich", + "stable-baselines3<3.0,>=2.4.0a4", + "tensorflow-probability", + "tqdm", +] +files = [ + {file = "sbx_rl-0.18.0-py3-none-any.whl", hash = "sha256:75ade634a33555ad4c4a81523bb0f99c89d1b3bc89fb74990ef87b22379abd9c"}, + {file = "sbx_rl-0.18.0.tar.gz", hash = "sha256:670f2bf095ec21ba6f8171602294baf0123787fe7be6811ebab276fb5010b8b3"}, +] + [[package]] name = "scipy" version = "1.14.1" @@ -1695,6 +1716,23 @@ files = [ {file = "scooby-0.10.0.tar.gz", hash = "sha256:7ea33c262c0cc6a33c6eeeb5648df787be4f22660e53c114e5fff1b811a8854f"}, ] +[[package]] +name = "seaborn" +version = "0.13.2" +requires_python = ">=3.8" +summary = "Statistical data visualization" +groups = ["default"] +marker = "python_version >= \"3.12\" and python_version < \"3.13\"" +dependencies = [ + "matplotlib!=3.6.1,>=3.4", + "numpy!=1.24.0,>=1.20", + "pandas>=1.2", +] +files = [ + {file = "seaborn-0.13.2-py3-none-any.whl", hash = "sha256:636f8336facf092165e27924f223d3c62ca560b1f2bb5dff7ab7fad265361987"}, + {file = "seaborn-0.13.2.tar.gz", hash = "sha256:93e60a40988f4d65e9f4885df477e2fdaff6b73a9ded434c1ab356dd57eefff7"}, +] + [[package]] name = "setuptools" version = "75.6.0" @@ -1809,6 +1847,26 @@ files = [ {file = "sympy-1.13.1.tar.gz", hash = "sha256:9cebf7e04ff162015ce31c9c6c9144daa34a93bd082f54fd8f12deca4f47515f"}, ] +[[package]] +name = "tensorflow-probability" +version = "0.25.0" +requires_python = ">=3.9" +summary = "Probabilistic modeling and statistical inference in TensorFlow" +groups = ["default"] +marker = "python_version >= \"3.12\" and python_version < \"3.13\"" +dependencies = [ + "absl-py", + "cloudpickle>=1.3", + "decorator", + "dm-tree", + "gast>=0.3.2", + "numpy>=1.13.3", + "six>=1.10.0", +] +files = [ + {file = "tensorflow_probability-0.25.0-py2.py3-none-any.whl", hash = "sha256:f3f4d6431656c0122906888afe1b67b4400e82bd7f254b45b92e6c5b84ea8e3e"}, +] + [[package]] name = "tensorstore" version = "0.1.71" @@ -1899,6 +1957,21 @@ files = [ {file = "tornado-6.4.2.tar.gz", hash = "sha256:92bad5b4746e9879fd7bf1eb21dce4e3fc5128d71601f80005afa39237ad620b"}, ] +[[package]] +name = "tqdm" +version = "4.67.1" +requires_python = ">=3.7" +summary = "Fast, Extensible Progress Meter" +groups = ["default"] +marker = "python_version >= \"3.12\" and python_version < \"3.13\"" +dependencies = [ + "colorama; platform_system == \"Windows\"", +] +files = [ + {file = "tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2"}, + {file = "tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2"}, +] + [[package]] name = "traitlets" version = "5.14.3" diff --git a/pyproject.toml b/pyproject.toml index 3ac6c98..1f5c82b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ description = "A solar car racing simulation library and GUI tool" authors = [ {name = "saji", email = "saji@saji.dev"}, ] -dependencies = ["pyqtgraph>=0.13.7", "jax[cuda12]>=0.4.37", "pytest>=8.3.3", "pyside6>=6.8.0.2", "matplotlib>=3.9.2", "gymnasium[jax]>=1.0.0", "pyvista>=0.44.2", "pyvistaqt>=0.11.1", "stable-baselines3>=2.4.0"] +dependencies = ["pyqtgraph>=0.13.7", "jax>=0.4.37", "pytest>=8.3.3", "pyside6>=6.8.0.2", "matplotlib>=3.9.2", "gymnasium[jax]>=1.0.0", "pyvista>=0.44.2", "pyvistaqt>=0.11.1", "stable-baselines3>=2.4.0", "gymnax>=0.0.8", "sbx-rl>=0.18.0"] requires-python = ">=3.10,<3.13" readme = "README.md" license = {text = "MIT"} diff --git a/report/report.tex b/report/report.tex new file mode 100644 index 0000000..4cddc6c --- /dev/null +++ b/report/report.tex @@ -0,0 +1,136 @@ +\documentclass[11pt]{article} + +% Essential packages +\usepackage[utf8]{inputenc} +\usepackage[T1]{fontenc} +\usepackage{amsmath,amssymb} +\usepackage{graphicx} +\usepackage[margin=1in]{geometry} +\usepackage{hyperref} +\usepackage{algorithm} +\usepackage{algorithmic} +\usepackage{float} +\usepackage{booktabs} +\usepackage{caption} +\usepackage{subcaption} + +% Custom commands +\newcommand{\sectionheading}[1]{\noindent\textbf{#1}} + +% Title and author information +\title{\Large{Your Project Title: A Study in Optimal Control and Reinforcement Learning}} +\author{Your Name\\ + Course Name\\ + Institution} +\date{\today} + +\begin{document} + +\maketitle + +\begin{abstract} +Solar Racing is a competition with the goal of creating highly efficient solar-assisted electric vehicles. Effective solar racing +requires awareness and complex decision making to determine optimal speeds to exploit the environmental conditions, such as winds, +cloud cover, and changes in elevation. We present an environment modelled on the dynamics involved for a race, including generated +elevation and wind profiles. The model uses the \texttt{gymnasium} interface to allow it to be used by a variety of algorithms. +We demonstrate a method of designing reward functions for multi-objective problems. The environment shows to be solvable by modern +reinforcement learning algorithms. +\end{abstract} + +\section{Introduction} +Start with a broad context of your problem area in optimal control/reinforcement learning. Then narrow down to your specific focus. Include: +\begin{itemize} + \item Problem motivation + \item Brief overview of existing approaches + \item Your specific contributions + \item Paper organization +\end{itemize} + +Solar racing was invented in the early 90s as a technology incubator for high-efficiency motor vehicles. The first solar races were speed +focused, however a style of race that focused on minimal energy use within a given route was developed to push focus towards vehicle efficiency. +The goal of these races is to arrive at a destination within a given time frame, while using as little grid (non-solar) energy as possible. +Aerodynamic drag is one of the most significant sources of energy consumption, along with elevation changes. The simplest policy to meet +the constraints of on-time arrival is: +$$ +V_{\text{avg}} = \frac{D}{T} +$$ +Where $D$ is the distance needed to travel, and $T$ is the maximum allowed time. + +\section{Background} +Provide necessary background on: +\begin{itemize} + \item Your specific application domain + \item Relevant algorithms and methods + \item Previous work in this area +\end{itemize} + +\section{Methodology} +Describe your approach in detail: +\begin{itemize} + \item Problem formulation + \item Algorithm description + \item Implementation details +\end{itemize} + +% Example of how to include an algorithm +\begin{algorithm}[H] +\caption{Your Algorithm Name} +\begin{algorithmic}[1] +\STATE Initialize parameters +\WHILE{not converged} + \STATE Update step +\ENDWHILE +\RETURN Result +\end{algorithmic} +\end{algorithm} + +\section{Experiments and Results} +Present your findings: +\begin{itemize} + \item Experimental setup + \item Results and analysis + \item Comparison with baselines (if applicable) +\end{itemize} + +% Example of how to include figures +\begin{figure}[H] +\centering +\caption{Description of your figure} +\label{fig:example} +\end{figure} + +% Example of how to include tables +\begin{table}[H] +\centering +\caption{Your Table Caption} +\begin{tabular}{lcc} +\toprule +Method & Metric 1 & Metric 2 \\ +\midrule +Approach 1 & Value & Value \\ +Approach 2 & Value & Value \\ +\bottomrule +\end{tabular} +\label{tab:results} +\end{table} + +\section{Discussion} +Analyze your results: +\begin{itemize} + \item Interpretation of findings + \item Limitations and challenges + \item Potential improvements +\end{itemize} + +\section{Conclusion} +Summarize your work: +\begin{itemize} + \item Key contributions + \item Practical implications + \item Future work directions +\end{itemize} + +\bibliography{references} +\bibliographystyle{plain} + +\end{document} diff --git a/src/solarcarsim/physsim.py b/src/solarcarsim/physsim.py index 368f6db..da14ba1 100644 --- a/src/solarcarsim/physsim.py +++ b/src/solarcarsim/physsim.py @@ -115,7 +115,6 @@ def bldc_power_draw(torque, velocity, params: MotorParams): return total_power -# @partial(jit, static_argnames=['resistance', 'kt', 'kv', 'vmax', 'Cf']) @jit def bldc_torque(velocity, current_limit, resistance, kt, kv, vmax, Cf): bemf = velocity / kv @@ -132,7 +131,6 @@ def bldc_torque(velocity, current_limit, resistance, kt, kv, vmax, Cf): @partial( jit, static_argnums=( - 1, 2, ), ) @@ -144,47 +142,12 @@ def battery_powerloss(current, cell_r, battery_shape: Tuple[int, int]): return jnp.sum(cell_Ploss) -def forward(state, timestep, control, params: CarParams): - # state is (position, time, energy) - # control is velocity - # timestep is >0 time to advance - # params is the params dictionary. - # returns the next state with (position', time + timestep, energy') - # TODO: terrain, weather, solar - - # determine the forces acting on the car. - dragf = drag_force(control, params.frontal_area, params.drag_coeff, 1.184) - rollf = rolling_force(params.mass, 0, params.rolling_coeff) - hillforce = downslope_force(params.mass, 0) - totalf = dragf + rollf + hillforce - # determine the power needed to make this force - tau = params.wheel_radius * totalf - pdraw = bldc_power_draw(tau, control, params.motor) - net_power = 0 - pdraw # watts aka j/s - - # TODO: calculate battery-based power losses. - # TODO: support regenerative braking when going downhill - # TODO: delta x = cos(theta) * velocity * timestep - - new_state = jnp.array( - [ - state[0] + control * timestep, - state[1] + timestep, - state[2] + net_power * timestep, - ] - ) - return new_state - - def make_environment(seed): """Generate a race environment: terrain function, wind function, wrapped forward function.""" - key, subkey = jax.random.split(seed) - wind = generate_wind_field(subkey, 10000, 600, spatial_scale=1000) - key, subkey = jax.random.split(key) - slope = fractal_noise_1d(subkey, 10000, scale=1200, height_scale=0.08) + windkey, slopekey = jax.random.split(seed, 2) + wind = generate_wind_field(windkey, 10000, 600, spatial_scale=1000) + slope = fractal_noise_1d(slopekey, 10000, scale=1200, height_scale=0.08) elevation = jnp.cumsum(slope) - # elevation = generate_elevation_profile(subkey, 10000, height_variation=40.0, scale=1200, octaves=5) - # slope = jnp.arctan(jnp.diff(elevation, prepend=100.0)) # rise/run return wind, elevation, slope diff --git a/src/solarcarsim/simv1.py b/src/solarcarsim/simv1.py index dee29b8..47fe60c 100644 --- a/src/solarcarsim/simv1.py +++ b/src/solarcarsim/simv1.py @@ -4,11 +4,9 @@ import jax import jax.numpy as jnp from jax import jit from functools import partial -from solarcarsim.physsim import drag_force, rolling_force, downslope_force, bldc_power_draw - @partial(jit, static_argnames=["params"]) -def forwardv2(state, control, delta_time, wind, elevation, slope, params): +def forward(state, control, delta_time, wind, elevation, slope, params: sim.CarParams): pos = jnp.astype(jnp.round(state[0]), "int32") time = jnp.astype(jnp.round(state[1]), "int32") theta = slope[pos] @@ -23,23 +21,22 @@ def forwardv2(state, control, delta_time, wind, elevation, slope, params): totalf = dragf + rollf + hillforce # with the sum of forces, determine the needed torque at the wheels, and then power tau = params.wheel_radius * totalf - pdraw = bldc_power_draw(tau, velocity, params.motor) + pdraw = sim.bldc_power_draw(tau, velocity, params.motor) # determine the energy needed to do this power for the time step net_power = state[2] - delta_time * pdraw # joules - dpos = jnp.cos(theta) * velocity * delta_time - dist_remaining = 10000.0 - dpos + dpos = state[0] + jnp.cos(theta) * velocity * delta_time + new_pos = jnp.maximum(dpos, 0) + dist_remaining = 10000.0 - (state[0] + dpos) time_remaining = 600 - (state[1] + delta_time) return jnp.array( - [dpos, state[1] + delta_time, net_power, dist_remaining, time_remaining] + [new_pos, state[1] + delta_time, net_power, dist_remaining, time_remaining] ) -def reward(state, prev_energy): - progress = state[0] / 10000 * 100 - energy_usage = 10 * (state[2] - prev_energy) # current energy < previous energy. - time_factor = (1.0 - (state[1] / 600)) * 50 - reward = progress + energy_usage + time_factor +def reward(state, prev_state): + reward = 0 + reward += state[0]/8000 return reward class SolarRaceV1(gym.Env): @@ -53,7 +50,6 @@ class SolarRaceV1(gym.Env): # self._state = jnp.array([np.array([x], dtype="float32") for x in (0,0,0, 10000.0, 600.0)]) self._state = jnp.array([[0],[0],[0],[10000.0], [600.0]]) # self._state = jnp.array([0, 0,0,10000.0, 600.0]) - def _vision_function(self): # extract the vision results. def slookup(x): @@ -81,7 +77,7 @@ class SolarRaceV1(gym.Env): self._reset_sim(jax.random.key(seed)) self._timestep = timestep self._car = car - self._simstep = forwardv2 + self._simstep = forward self._simreward = reward self.observation_space = gym.spaces.Dict( @@ -108,15 +104,20 @@ class SolarRaceV1(gym.Env): def step(self, action): wind, elevation, slope = self._environment - old_energy = self._state[2] + old_state = self._state self._state = self._simstep(self._state, action, self._timestep,wind, elevation, slope, self._car) - reward = self._simreward(self._state, old_energy)[0] + reward = self._simreward(self._state, old_state)[0] terminated = False truncated = False - if jnp.all(self._state[0] > 10000): + if jnp.all(self._state[0] > 8000): + reward += 500 terminated = True - if self._state[1] > 600: + # we want the time to be as close to 600 as possible + reward -= 600 - self._state[1][0] + reward += 1e-6 * (self._state[2][0]) # net energy is negative. + if jnp.all(self._state[1] > 600): + reward -= 50000 truncated = True return self._get_obs(), reward, terminated, truncated, {} diff --git a/src/solarcarsim/simv2.py b/src/solarcarsim/simv2.py index 60a66d2..fffe8c0 100644 --- a/src/solarcarsim/simv2.py +++ b/src/solarcarsim/simv2.py @@ -1,14 +1,190 @@ -""" Second-generation simulator. More functional, cleaner code, faster """ +"""Second-generation simulator. More functional, cleaner code, faster""" -from typing import NamedTuple +from typing import NamedTuple, Optional, Tuple, Union, Dict, Any import jax import jax.numpy as jnp +import chex +from flax import struct +from jax import lax +from gymnax.environments import environment +from gymnax.environments import spaces + +from solarcarsim.physsim import CarParams, fractal_noise_1d +import solarcarsim.physsim as sim -class SimState(NamedTuple): - position: float - time: float - energy: float - distance_remaining: float - time_remaining: float +@struct.dataclass +class SimState(environment.EnvState): + position: jnp.ndarray + velocity: jnp.ndarray + realtime: jnp.ndarray + energy: jnp.ndarray + # distance_remaining: jnp.ndarray + # time_remaining: jnp.ndarray + slope: jnp.ndarray + +@struct.dataclass +class SimParams(environment.EnvParams): + car: CarParams = CarParams() + goal_time: int = 600 + goal_dist: int = 8000 + map_size: int = 10000 + time_step: float = 1.0 + terrain_lookahead: int = 100 + # skip wind for now + + +class Snax(environment.Environment[SimState, SimParams]): + """JAX version of the solar race simulator""" + + @property + def default_params(self) -> SimParams: + return SimParams() + + def action_space(self, params: Optional[SimParams] = None): + return spaces.Box(low=-1.0, high=1.0, shape=(1,)) + + def observation_space(self, params: Optional[SimParams] = None) -> spaces.Box: + if params is None: + params = self.default_params + # needs to be a box. it will be [pos, time, energy, dist_to_goal, time_remaining, terrain0, terrain1] + shape = 5 + params.terrain_lookahead + low = jnp.array( + [0, 0, -1e11, 0, 0] + [-1.0] * params.terrain_lookahead, dtype=jnp.float32 + ) + high = jnp.array( + [params.map_size, params.goal_time, 0, params.goal_dist, params.goal_time] + + [1.0] * params.terrain_lookahead, + dtype=jnp.float32, + ) + return spaces.Box(low, high, shape=(shape,)) + # return spaces.Dict( + # { + # "position": spaces.Box(0.0, params.map_size, (), jnp.float32), + # "realtime": spaces.Box(0.0, params.goal_time + 100, (), jnp.float32), + # "energy": spaces.Box(-1e11, 0.0, (), jnp.float32), + # "dist_to_goal": spaces.Box(0.0, params.goal_dist, (), jnp.float32), + # "time_remaining": spaces.Box(0.0, params.goal_time, (), jnp.float32), + # "upcoming_terrain": spaces.Box( + # -1.0, 1.0, shape=(100,), dtype=jnp.float32 + # ), + # # skip wind for now + # } + # ) + + def state_space(self, params: Optional[SimParams] = None) -> spaces.Dict: + if params is None: + params = self.default_params + return spaces.Dict( + { + "position": spaces.Box(0.0, params.map_size, (), jnp.float32), + "realtime": spaces.Box(0.0, params.goal_time + 100, (), jnp.float32), + "energy": spaces.Box(-1e11, 0.0, (), jnp.float32), + # "dist_to_goal": spaces.Box(0.0, params.goal_dist, (), jnp.float32), + # "time_remaining": spaces.Box(0.0, params.goal_time, (), jnp.float32), + "slope": spaces.Box( + -1.0, 1.0, shape=(params.map_size,), dtype=jnp.float32 + ), + "time": spaces.Discrete(int(params.goal_time / params.time_step)), + } + ) + + def reset_env( + self, key: chex.PRNGKey, params: Optional[SimParams] = None + ) -> Tuple[chex.Array, SimState]: + if params is None: + params = self.default_params + slope = fractal_noise_1d(key, 10000, scale=1200, height_scale=0.08) + init_state = SimState( + position=jnp.array(0.0), + velocity=jnp.array(0.0), + time=0, + realtime=jnp.array(0.0), + energy=jnp.array(0.0), + # distance_remaining=jnp.array(params.goal_dist), + # time_remaining=jnp.array(params.goal_time), + slope=slope, + ) + return self.get_obs(init_state, key, params), init_state + + def get_obs( + self, state: SimState, key: chex.PRNGKey, params: SimParams + ) -> chex.Array: + if params is None: + params = self.default_params + + # get rounded position from state + pos_int = jnp.astype(state.position, jnp.int32) + + terrain_view = jax.lax.dynamic_slice(state.slope, (pos_int,), (100,)) + dist_to_goal = jnp.abs(params.goal_dist - state.position) + time_remaining = jnp.abs(params.goal_time - state.realtime) + main_state = jnp.array( + [state.position, state.realtime, state.energy, dist_to_goal, time_remaining] + ) + return jnp.concat([main_state, terrain_view]).squeeze() + + def step_env( + self, + key: chex.PRNGKey, + state: SimState, + action: Union[int, float, chex.Array], + params: SimParams, + ) -> Tuple[chex.Array, SimState, jnp.ndarray, jnp.ndarray, Dict[Any, Any]]: + pos = jnp.astype(state.position, jnp.int32) + theta = state.slope[pos] + velocity = jnp.array([action * params.car.max_speed]).squeeze() + dragf = sim.drag_force( + velocity, params.car.frontal_area, params.car.drag_coeff, 1.184 + ) + rollf = sim.rolling_force(params.car.mass, theta, params.car.rolling_coeff) + hillf = sim.downslope_force(params.car.mass, theta) + total_f = dragf + rollf + hillf + tau = params.car.wheel_radius * total_f / params.car.n_motors + p_draw = ( + sim.bldc_power_draw(tau, velocity, params.car.motor) * params.car.n_motors + ) + + new_energy = state.energy - params.time_step * p_draw + new_position = state.position + jnp.cos(theta) * velocity * params.time_step + new_state = SimState( + position=new_position.squeeze(), + velocity=velocity.squeeze(), + realtime=state.realtime + params.time_step, + energy=new_energy.squeeze(), + slope=state.slope, + time=state.time + 1, + ) + + # compute reward + # reward = new_state.position / params.goal_dist + # if new_state.position >= params.goal_dist: + # reward += 100 + # reward += params.goal_time - new_state.realtime + # # penalize energy use + # reward += 1e-7 * new_state.energy # energy is negative + # if ( + # new_state.realtime >= params.goal_time + # or new_state.time > params.max_steps_in_episode + # ): + # reward -= 500 + + # we have to vectorize that. + reward = new_state.position / params.goal_dist + \ + (new_state.position >= params.goal_dist) * (100 + params.goal_time - new_state.realtime + 1e-7*new_state.energy) + \ + (new_state.realtime >= params.goal_time) * -500 + reward = reward.squeeze() + terminal = self.is_terminal(state, params) + return ( + lax.stop_gradient(self.get_obs(new_state, key, params)), + lax.stop_gradient(new_state), + reward, + terminal, + {}, + ) + + def is_terminal(self, state: SimState, params: SimParams) -> jnp.ndarray: + finish = state.position >= params.goal_dist + timeout = state.time >= params.max_steps_in_episode + return jnp.logical_or(finish, timeout).squeeze()