diff --git a/henry_experiments/pretrained_checkpoints/gc_pgp_checkpoint.ckpt b/experiments/pretrained_checkpoints/gc_pgp_checkpoint.ckpt
similarity index 100%
rename from henry_experiments/pretrained_checkpoints/gc_pgp_checkpoint.ckpt
rename to experiments/pretrained_checkpoints/gc_pgp_checkpoint.ckpt
diff --git a/henry_experiments/pretrained_checkpoints/pdm_offset_checkpoint.ckpt b/experiments/pretrained_checkpoints/pdm_offset_checkpoint.ckpt
similarity index 100%
rename from henry_experiments/pretrained_checkpoints/pdm_offset_checkpoint.ckpt
rename to experiments/pretrained_checkpoints/pdm_offset_checkpoint.ckpt
diff --git a/henry_experiments/pretrained_checkpoints/urbandriver_checkpoint.ckpt b/experiments/pretrained_checkpoints/urbandriver_checkpoint.ckpt
similarity index 100%
rename from henry_experiments/pretrained_checkpoints/urbandriver_checkpoint.ckpt
rename to experiments/pretrained_checkpoints/urbandriver_checkpoint.ckpt
diff --git a/henry_experiments/relavance_construction.ipynb b/experiments/relavance_construction.ipynb
similarity index 100%
rename from henry_experiments/relavance_construction.ipynb
rename to experiments/relavance_construction.ipynb
diff --git a/henry_experiments/run_sim_closed_loop.ipynb b/experiments/run_sim_closed_loop.ipynb
similarity index 100%
rename from henry_experiments/run_sim_closed_loop.ipynb
rename to experiments/run_sim_closed_loop.ipynb
diff --git a/henry_experiments/run_sim_closed_loop/training_raster_experiment/train_default_raster/2023.11.14.22.55.23/hparams.yaml b/experiments/run_sim_closed_loop/training_raster_experiment/train_default_raster/2023.11.14.22.55.23/hparams.yaml
similarity index 100%
rename from henry_experiments/run_sim_closed_loop/training_raster_experiment/train_default_raster/2023.11.14.22.55.23/hparams.yaml
rename to experiments/run_sim_closed_loop/training_raster_experiment/train_default_raster/2023.11.14.22.55.23/hparams.yaml
diff --git a/henry_experiments/run_sim_closed_loop/training_raster_experiment/train_default_raster/2023.11.15.01.28.02/hparams.yaml b/experiments/run_sim_closed_loop/training_raster_experiment/train_default_raster/2023.11.15.01.28.02/hparams.yaml
similarity index 100%
rename from henry_experiments/run_sim_closed_loop/training_raster_experiment/train_default_raster/2023.11.15.01.28.02/hparams.yaml
rename to experiments/run_sim_closed_loop/training_raster_experiment/train_default_raster/2023.11.15.01.28.02/hparams.yaml
diff --git a/experiments/test_notebook.ipynb b/experiments/test_notebook.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..33cfffacf3c5efe3c7a2472c4d336291b7d28ba9
--- /dev/null
+++ b/experiments/test_notebook.ipynb
@@ -0,0 +1,389 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "id": "34460db1",
+   "metadata": {},
+   "source": [
+    "# Creating a new planner in nuPlan <a name=\"introduction\"></a>\n"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "f0189156",
+   "metadata": {},
+   "source": [
+    "## Setup"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 1,
+   "id": "373ffd1c",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stderr",
+     "output_type": "stream",
+     "text": [
+      "/tmp/ipykernel_5670/4095267831.py:5: DeprecationWarning: Importing display from IPython.core.display is deprecated since IPython 7.14, please import from IPython display\n",
+      "  from IPython.core.display import display, HTML\n"
+     ]
+    }
+   ],
+   "source": [
+    "# Useful imports\n",
+    "import os\n",
+    "import hydra\n",
+    "import nest_asyncio\n",
+    "from IPython.core.display import display, HTML\n",
+    "from bokeh.io import output_notebook\n"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 2,
+   "id": "13c00121",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/html": [
+       "<div class=\"bk-root\">\n",
+       "        <a href=\"https://bokeh.org\" target=\"_blank\" class=\"bk-logo bk-logo-small bk-logo-notebook\"></a>\n",
+       "        <span id=\"1001\">Loading BokehJS ...</span>\n",
+       "    </div>\n"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "application/javascript": "(function(root) {\n  function now() {\n    return new Date();\n  }\n\n  const force = true;\n\n  if (typeof root._bokeh_onload_callbacks === \"undefined\" || force === true) {\n    root._bokeh_onload_callbacks = [];\n    root._bokeh_is_loading = undefined;\n  }\n\nconst JS_MIME_TYPE = 'application/javascript';\n  const HTML_MIME_TYPE = 'text/html';\n  const EXEC_MIME_TYPE = 'application/vnd.bokehjs_exec.v0+json';\n  const CLASS_NAME = 'output_bokeh rendered_html';\n\n  /**\n   * Render data to the DOM node\n   */\n  function render(props, node) {\n    const script = document.createElement(\"script\");\n    node.appendChild(script);\n  }\n\n  /**\n   * Handle when an output is cleared or removed\n   */\n  function handleClearOutput(event, handle) {\n    const cell = handle.cell;\n\n    const id = cell.output_area._bokeh_element_id;\n    const server_id = cell.output_area._bokeh_server_id;\n    // Clean up Bokeh references\n    if (id != null && id in Bokeh.index) {\n      Bokeh.index[id].model.document.clear();\n      delete Bokeh.index[id];\n    }\n\n    if (server_id !== undefined) {\n      // Clean up Bokeh references\n      const cmd_clean = \"from bokeh.io.state import curstate; print(curstate().uuid_to_server['\" + server_id + \"'].get_sessions()[0].document.roots[0]._id)\";\n      cell.notebook.kernel.execute(cmd_clean, {\n        iopub: {\n          output: function(msg) {\n            const id = msg.content.text.trim();\n            if (id in Bokeh.index) {\n              Bokeh.index[id].model.document.clear();\n              delete Bokeh.index[id];\n            }\n          }\n        }\n      });\n      // Destroy server and session\n      const cmd_destroy = \"import bokeh.io.notebook as ion; ion.destroy_server('\" + server_id + \"')\";\n      cell.notebook.kernel.execute(cmd_destroy);\n    }\n  }\n\n  /**\n   * Handle when a new output is added\n   */\n  function handleAddOutput(event, handle) {\n    const output_area = handle.output_area;\n    const output = handle.output;\n\n    // limit handleAddOutput to display_data with EXEC_MIME_TYPE content only\n    if ((output.output_type != \"display_data\") || (!Object.prototype.hasOwnProperty.call(output.data, EXEC_MIME_TYPE))) {\n      return\n    }\n\n    const toinsert = output_area.element.find(\".\" + CLASS_NAME.split(' ')[0]);\n\n    if (output.metadata[EXEC_MIME_TYPE][\"id\"] !== undefined) {\n      toinsert[toinsert.length - 1].firstChild.textContent = output.data[JS_MIME_TYPE];\n      // store reference to embed id on output_area\n      output_area._bokeh_element_id = output.metadata[EXEC_MIME_TYPE][\"id\"];\n    }\n    if (output.metadata[EXEC_MIME_TYPE][\"server_id\"] !== undefined) {\n      const bk_div = document.createElement(\"div\");\n      bk_div.innerHTML = output.data[HTML_MIME_TYPE];\n      const script_attrs = bk_div.children[0].attributes;\n      for (let i = 0; i < script_attrs.length; i++) {\n        toinsert[toinsert.length - 1].firstChild.setAttribute(script_attrs[i].name, script_attrs[i].value);\n        toinsert[toinsert.length - 1].firstChild.textContent = bk_div.children[0].textContent\n      }\n      // store reference to server id on output_area\n      output_area._bokeh_server_id = output.metadata[EXEC_MIME_TYPE][\"server_id\"];\n    }\n  }\n\n  function register_renderer(events, OutputArea) {\n\n    function append_mime(data, metadata, element) {\n      // create a DOM node to render to\n      const toinsert = this.create_output_subarea(\n        metadata,\n        CLASS_NAME,\n        EXEC_MIME_TYPE\n      );\n      this.keyboard_manager.register_events(toinsert);\n      // Render to node\n      const props = {data: data, metadata: metadata[EXEC_MIME_TYPE]};\n      render(props, toinsert[toinsert.length - 1]);\n      element.append(toinsert);\n      return toinsert\n    }\n\n    /* Handle when an output is cleared or removed */\n    events.on('clear_output.CodeCell', handleClearOutput);\n    events.on('delete.Cell', handleClearOutput);\n\n    /* Handle when a new output is added */\n    events.on('output_added.OutputArea', handleAddOutput);\n\n    /**\n     * Register the mime type and append_mime function with output_area\n     */\n    OutputArea.prototype.register_mime_type(EXEC_MIME_TYPE, append_mime, {\n      /* Is output safe? */\n      safe: true,\n      /* Index of renderer in `output_area.display_order` */\n      index: 0\n    });\n  }\n\n  // register the mime type if in Jupyter Notebook environment and previously unregistered\n  if (root.Jupyter !== undefined) {\n    const events = require('base/js/events');\n    const OutputArea = require('notebook/js/outputarea').OutputArea;\n\n    if (OutputArea.prototype.mime_types().indexOf(EXEC_MIME_TYPE) == -1) {\n      register_renderer(events, OutputArea);\n    }\n  }\n  if (typeof (root._bokeh_timeout) === \"undefined\" || force === true) {\n    root._bokeh_timeout = Date.now() + 5000;\n    root._bokeh_failed_load = false;\n  }\n\n  const NB_LOAD_WARNING = {'data': {'text/html':\n     \"<div style='background-color: #fdd'>\\n\"+\n     \"<p>\\n\"+\n     \"BokehJS does not appear to have successfully loaded. If loading BokehJS from CDN, this \\n\"+\n     \"may be due to a slow or bad network connection. Possible fixes:\\n\"+\n     \"</p>\\n\"+\n     \"<ul>\\n\"+\n     \"<li>re-rerun `output_notebook()` to attempt to load from CDN again, or</li>\\n\"+\n     \"<li>use INLINE resources instead, as so:</li>\\n\"+\n     \"</ul>\\n\"+\n     \"<code>\\n\"+\n     \"from bokeh.resources import INLINE\\n\"+\n     \"output_notebook(resources=INLINE)\\n\"+\n     \"</code>\\n\"+\n     \"</div>\"}};\n\n  function display_loaded() {\n    const el = document.getElementById(\"1001\");\n    if (el != null) {\n      el.textContent = \"BokehJS is loading...\";\n    }\n    if (root.Bokeh !== undefined) {\n      if (el != null) {\n        el.textContent = \"BokehJS \" + root.Bokeh.version + \" successfully loaded.\";\n      }\n    } else if (Date.now() < root._bokeh_timeout) {\n      setTimeout(display_loaded, 100)\n    }\n  }\n\n  function run_callbacks() {\n    try {\n      root._bokeh_onload_callbacks.forEach(function(callback) {\n        if (callback != null)\n          callback();\n      });\n    } finally {\n      delete root._bokeh_onload_callbacks\n    }\n    console.debug(\"Bokeh: all callbacks have finished\");\n  }\n\n  function load_libs(css_urls, js_urls, callback) {\n    if (css_urls == null) css_urls = [];\n    if (js_urls == null) js_urls = [];\n\n    root._bokeh_onload_callbacks.push(callback);\n    if (root._bokeh_is_loading > 0) {\n      console.debug(\"Bokeh: BokehJS is being loaded, scheduling callback at\", now());\n      return null;\n    }\n    if (js_urls == null || js_urls.length === 0) {\n      run_callbacks();\n      return null;\n    }\n    console.debug(\"Bokeh: BokehJS not loaded, scheduling load and callback at\", now());\n    root._bokeh_is_loading = css_urls.length + js_urls.length;\n\n    function on_load() {\n      root._bokeh_is_loading--;\n      if (root._bokeh_is_loading === 0) {\n        console.debug(\"Bokeh: all BokehJS libraries/stylesheets loaded\");\n        run_callbacks()\n      }\n    }\n\n    function on_error(url) {\n      console.error(\"failed to load \" + url);\n    }\n\n    for (let i = 0; i < css_urls.length; i++) {\n      const url = css_urls[i];\n      const element = document.createElement(\"link\");\n      element.onload = on_load;\n      element.onerror = on_error.bind(null, url);\n      element.rel = \"stylesheet\";\n      element.type = \"text/css\";\n      element.href = url;\n      console.debug(\"Bokeh: injecting link tag for BokehJS stylesheet: \", url);\n      document.body.appendChild(element);\n    }\n\n    for (let i = 0; i < js_urls.length; i++) {\n      const url = js_urls[i];\n      const element = document.createElement('script');\n      element.onload = on_load;\n      element.onerror = on_error.bind(null, url);\n      element.async = false;\n      element.src = url;\n      console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n      document.head.appendChild(element);\n    }\n  };\n\n  function inject_raw_css(css) {\n    const element = document.createElement(\"style\");\n    element.appendChild(document.createTextNode(css));\n    document.body.appendChild(element);\n  }\n\n  const js_urls = [\"https://cdn.bokeh.org/bokeh/release/bokeh-2.4.3.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-gl-2.4.3.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-widgets-2.4.3.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-tables-2.4.3.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-mathjax-2.4.3.min.js\"];\n  const css_urls = [];\n\n  const inline_js = [    function(Bokeh) {\n      Bokeh.set_log_level(\"info\");\n    },\nfunction(Bokeh) {\n    }\n  ];\n\n  function run_inline_js() {\n    if (root.Bokeh !== undefined || force === true) {\n          for (let i = 0; i < inline_js.length; i++) {\n      inline_js[i].call(root, root.Bokeh);\n    }\nif (force === true) {\n        display_loaded();\n      }} else if (Date.now() < root._bokeh_timeout) {\n      setTimeout(run_inline_js, 100);\n    } else if (!root._bokeh_failed_load) {\n      console.log(\"Bokeh: BokehJS failed to load within specified timeout.\");\n      root._bokeh_failed_load = true;\n    } else if (force !== true) {\n      const cell = $(document.getElementById(\"1001\")).parents('.cell').data().cell;\n      cell.output_area.append_execute_result(NB_LOAD_WARNING)\n    }\n  }\n\n  if (root._bokeh_is_loading === 0) {\n    console.debug(\"Bokeh: BokehJS loaded, going straight to plotting\");\n    run_inline_js();\n  } else {\n    load_libs(css_urls, js_urls, function() {\n      console.debug(\"Bokeh: BokehJS plotting callback run at\", now());\n      run_inline_js();\n    });\n  }\n}(window));",
+      "application/vnd.bokehjs_load.v0+json": ""
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "<style>.output_result { max-width:100% !important; }</style>"
+      ],
+      "text/plain": [
+       "<IPython.core.display.HTML object>"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "<style>.container { width:100% !important; }</style>"
+      ],
+      "text/plain": [
+       "<IPython.core.display.HTML object>"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    }
+   ],
+   "source": [
+    "nest_asyncio.apply()\n",
+    "output_notebook()\n",
+    "display(HTML(\"<style>.output_result { max-width:100% !important; }</style>\"))\n",
+    "display(HTML(\"<style>.container { width:100% !important; }</style>\"))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "128b0d15",
+   "metadata": {},
+   "source": [
+    "# Simulating the planner <a name=\"simulation\"></a>"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "db337ceb",
+   "metadata": {},
+   "source": [
+    "## Prepare the simulation config"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 3,
+   "id": "11b08c6d",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "from tutorials.utils.tutorial_utils import construct_simulation_hydra_paths\n",
+    "\n",
+    "# Location of paths with all simulation configs\n",
+    "BASE_CONFIG_PATH = os.path.join(os.getenv('NUPLAN_TUTORIAL_PATH', ''), '../nuplan/planning/script')\n",
+    "simulation_hydra_paths = construct_simulation_hydra_paths(BASE_CONFIG_PATH)\n",
+    "\n",
+    "# Create a temporary directory to store the simulation artifacts\n",
+    "\n",
+    "DATASET_PARAMS = [\n",
+    "    'scenario_builder=nuplan_mini',  # use nuplan mini database (2.5h of 8 autolabeled logs in Las Vegas)\n",
+    "    'scenario_filter=one_continuous_log',  # simulate only one log\n",
+    "    \"scenario_filter.log_names=['2021.06.14.16.48.02_veh-12_04057_04438']\",\n",
+    "    'scenario_filter.limit_total_scenarios=1',  # use 1 total scenarios\n",
+    "]\n",
+    "ckpt_dir = '/home/sacardoz/checkpoints/pdm_offset_checkpoint.ckpt'\n",
+    "#'/home/sacardoz/checkpoints/urbandriver_checkpoint.ckpt'\n",
+    "#\"/home/sacardoz/tutorial_vector_framework/training_simple_vector_experiment/train_default_simple_vector/2023.11.23.09.55.21/best_model/epoch.ckpt\"\n",
+    "#\"/home/sacardoz/training_raster_experiment/train_default_raster/2023.11.23.07.36.36/best_model/epoch.ckpt\"\n",
+    "# Initialize configuration management system\n",
+    "hydra.core.global_hydra.GlobalHydra.instance().clear()  # reinitialize hydra if already initialized\n",
+    "hydra.initialize(config_path=simulation_hydra_paths.config_path)\n",
+    "\n",
+    "# Compose the configuration\n",
+    "cfg = hydra.compose(config_name=simulation_hydra_paths.config_name, overrides=[\n",
+    "    '+simulation=closed_loop_reactive_agents',\n",
+    "    #'model=pgm_hybrid_model',\n",
+    "    'planner=pdm_hybrid_planner',\n",
+    "    f\"planner.pdm_hybrid_planner.checkpoint_path={ckpt_dir}\" ,\n",
+    "    #'planner.ml_planner.model_config=${model}',\n",
+    "    #f'planner.ml_planner.checkpoint_path={ckpt_dir}',\n",
+    "    #f'observation=idm_agents_observation',\n",
+    "    #'observation.model_config=${model}',\n",
+    "    #f'observation.checkpoint_path={ckpt_dir}',\n",
+    "    'worker=sequential',\n",
+    "    '+occlusion=true',\n",
+    "    \"hydra.searchpath=[pkg://tuplan_garage.planning.script.config.common, pkg://tuplan_garage.planning.script.config.simulation, pkg://nuplan.planning.script.config.common, pkg://nuplan.planning.script.experiments]\",\n",
+    "    *DATASET_PARAMS,\n",
+    "])"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "88231b74",
+   "metadata": {},
+   "source": [
+    "## Launch simulation (within the notebook)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 4,
+   "id": "161cc166",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stderr",
+     "output_type": "stream",
+     "text": [
+      "INFO:nuplan.planning.script.utils:Setting default NUPLAN_DATA_ROOT: /home/sacardoz/nuplan/dataset\n",
+      "INFO:nuplan.planning.script.utils:Setting default NUPLAN_EXP_ROOT: /home/sacardoz/nuplan/exp\n",
+      "Global seed set to 0\n",
+      "INFO:nuplan.planning.script.builders.main_callback_builder:Building MultiMainCallback...\n",
+      "INFO:nuplan.planning.script.builders.main_callback_builder:Building MultiMainCallback: 4...DONE!\n"
+     ]
+    },
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "2023-11-28 11:02:20,269 INFO {/home/sacardoz/nuplan-devkit/nuplan/planning/script/builders/worker_pool_builder.py:19}  Building WorkerPool...\n",
+      "2023-11-28 11:02:20,270 INFO {/home/sacardoz/nuplan-devkit/nuplan/planning/utils/multithreading/worker_pool.py:101}  Worker: Sequential\n",
+      "2023-11-28 11:02:20,270 INFO {/home/sacardoz/nuplan-devkit/nuplan/planning/utils/multithreading/worker_pool.py:102}  Number of nodes: 1\n",
+      "Number of CPUs per node: 1\n",
+      "Number of GPUs per node: 0\n",
+      "Number of threads across all nodes: 1\n",
+      "2023-11-28 11:02:20,270 INFO {/home/sacardoz/nuplan-devkit/nuplan/planning/script/builders/worker_pool_builder.py:27}  Building WorkerPool...DONE!\n",
+      "2023-11-28 11:02:20,270 INFO {/home/sacardoz/nuplan-devkit/nuplan/planning/script/builders/folder_builder.py:32}  Building experiment folders...\n",
+      "2023-11-28 11:02:20,270 INFO {/home/sacardoz/nuplan-devkit/nuplan/planning/script/builders/folder_builder.py:35}  \n",
+      "\n",
+      "\tFolder where all results are stored: /home/sacardoz/nuplan/exp/exp/simulation/closed_loop_reactive_agents/2023.11.28.11.02.19\n",
+      "\n",
+      "2023-11-28 11:02:20,271 INFO {/home/sacardoz/nuplan-devkit/nuplan/planning/script/builders/folder_builder.py:70}  Building experiment folders...DONE!\n",
+      "2023-11-28 11:02:20,272 INFO {/home/sacardoz/nuplan-devkit/nuplan/planning/script/builders/simulation_callback_builder.py:52}  Building AbstractCallback...\n",
+      "2023-11-28 11:02:20,272 INFO {/home/sacardoz/nuplan-devkit/nuplan/planning/script/builders/simulation_callback_builder.py:68}  Building AbstractCallback: 0...DONE!\n",
+      "2023-11-28 11:02:20,272 INFO {/home/sacardoz/nuplan-devkit/nuplan/planning/script/builders/simulation_builder.py:49}  Building simulations...\n",
+      "2023-11-28 11:02:20,272 INFO {/home/sacardoz/nuplan-devkit/nuplan/planning/script/builders/simulation_builder.py:55}  Extracting scenarios...\n",
+      "2023-11-28 11:02:20,272 INFO {/home/sacardoz/nuplan-devkit/nuplan/common/utils/distributed_scenario_filter.py:83}  Building Scenarios in mode DistributedMode.SINGLE_NODE\n",
+      "2023-11-28 11:02:20,272 INFO {/home/sacardoz/nuplan-devkit/nuplan/planning/script/builders/scenario_building_builder.py:18}  Building AbstractScenarioBuilder...\n",
+      "2023-11-28 11:02:20,284 INFO {/home/sacardoz/nuplan-devkit/nuplan/planning/script/builders/scenario_building_builder.py:21}  Building AbstractScenarioBuilder...DONE!\n",
+      "2023-11-28 11:02:20,284 INFO {/home/sacardoz/nuplan-devkit/nuplan/planning/script/builders/scenario_filter_builder.py:35}  Building ScenarioFilter...\n",
+      "2023-11-28 11:02:20,285 INFO {/home/sacardoz/nuplan-devkit/nuplan/planning/script/builders/scenario_filter_builder.py:44}  Building ScenarioFilter...DONE!\n",
+      "2023-11-28 11:02:20,302 INFO {/home/sacardoz/nuplan-devkit/nuplan/planning/script/builders/simulation_builder.py:76}  Building metric engines...\n",
+      "2023-11-28 11:02:20,327 INFO {/home/sacardoz/nuplan-devkit/nuplan/planning/script/builders/simulation_builder.py:78}  Building metric engines...DONE\n",
+      "2023-11-28 11:02:20,327 INFO {/home/sacardoz/nuplan-devkit/nuplan/planning/script/builders/simulation_builder.py:82}  Building simulations from 1 scenarios...\n",
+      "2023-11-28 11:02:20,786 INFO {/home/sacardoz/nuplan-devkit/nuplan/planning/script/builders/simulation_builder.py:142}  Building simulations...DONE!\n"
+     ]
+    }
+   ],
+   "source": [
+    "from nuplan.planning.script.run_simulation import build_simulation_runners\n",
+    "from nuplan.common.actor_state.tracked_objects_types import AGENT_TYPES, STATIC_OBJECT_TYPES, TrackedObjectType\n",
+    "\n",
+    "# Run the simulation loop (real-time visualization not yet supported, see next section for visualization)\n",
+    "runners, common_builder, cfg = build_simulation_runners(cfg)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 5,
+   "id": "223284d4",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "runner = runners[0]"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 6,
+   "id": "90b79421",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stderr",
+     "output_type": "stream",
+     "text": [
+      "                                                                                      \r"
+     ]
+    }
+   ],
+   "source": [
+    "runner.simulation.callback.on_simulation_start(runner.simulation.setup)\n",
+    "\n",
+    "# Initialize all simulations\n",
+    "runner._initialize()\n",
+    "\n",
+    "while runner.simulation.is_simulation_running():\n",
+    "    # Execute specific callback\n",
+    "    runner.simulation.callback.on_step_start(runner.simulation.setup, runner.planner)\n",
+    "\n",
+    "    # Perform step\n",
+    "    planner_input = runner._simulation.get_planner_input()\n",
+    "\n",
+    "    # Execute specific callback\n",
+    "    runner._simulation.callback.on_planner_start(runner.simulation.setup, runner.planner)\n",
+    "\n",
+    "    # Plan path based on all planner's inputs\n",
+    "    trajectory = runner.planner.compute_trajectory(planner_input)\n",
+    "\n",
+    "    # Propagate simulation based on planner trajectory\n",
+    "    runner._simulation.callback.on_planner_end(runner.simulation.setup, runner.planner, trajectory)\n",
+    "\n",
+    "    iteration = runner.simulation._time_controller.get_iteration()\n",
+    "\n",
+    "    runner.simulation.propagate(trajectory)\n",
+    "\n",
+    "    # Execute specific callback\n",
+    "    runner.simulation.callback.on_step_end(runner.simulation.setup, runner.planner, runner.simulation.history.last())\n",
+    "\n",
+    "runner.simulation.callback.on_simulation_end(runner.simulation.setup, runner.planner, runner.simulation.history)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 7,
+   "id": "e6c22f5f",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stderr",
+     "output_type": "stream",
+     "text": [
+      "INFO:bokeh.server.server:Starting Bokeh server version 2.4.3 (running on Tornado 6.3.3)\n",
+      "WARNING:bokeh.server.util:Host wildcard '*' will allow connections originating from multiple (or possibly all) hostnames or IPs. Use non-wildcard values to restrict access explicitly\n",
+      "INFO:bokeh.server.tornado:User authentication hooks NOT provided (default user enabled)\n"
+     ]
+    },
+    {
+     "data": {
+      "application/vnd.bokehjs_exec.v0+json": "",
+      "text/html": [
+       "<script id=\"1003\">\n",
+       "  (function() {\n",
+       "    const xhr = new XMLHttpRequest()\n",
+       "    xhr.responseType = 'blob';\n",
+       "    xhr.open('GET', \"http://localhost:8888/autoload.js?bokeh-autoload-element=1003&bokeh-absolute-url=http://localhost:8888&resources=none\", true);\n",
+       "    xhr.onload = function (event) {\n",
+       "      const script = document.createElement('script');\n",
+       "      const src = URL.createObjectURL(event.target.response);\n",
+       "      script.src = src;\n",
+       "      document.body.appendChild(script);\n",
+       "    };\n",
+       "    xhr.send();\n",
+       "  })();\n",
+       "</script>"
+      ]
+     },
+     "metadata": {
+      "application/vnd.bokehjs_exec.v0+json": {
+       "server_id": "09119f1b4c254247b9a8a2c9feb5a3bb"
+      }
+     },
+     "output_type": "display_data"
+    },
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "2023-11-28 11:03:23,128 INFO {/home/sacardoz/nuplan-devkit/nuplan/planning/nuboard/base/experiment_file_data.py:140}  Could not open Parquet input source '<Buffer>': Parquet magic bytes not found in footer. Either the file is corrupted or this is not a parquet file.\n",
+      "2023-11-28 11:03:23,129 INFO {/home/sacardoz/nuplan-devkit/nuplan/planning/nuboard/base/experiment_file_data.py:140}  Could not open Parquet input source '<Buffer>': Parquet magic bytes not found in footer. Either the file is corrupted or this is not a parquet file.\n",
+      "2023-11-28 11:03:23,129 INFO {/home/sacardoz/nuplan-devkit/nuplan/planning/nuboard/base/experiment_file_data.py:140}  Error creating dataset. Could not read schema from 'pretrained_checkpoints/gc_pgp_checkpoint.ckpt'. Is this a 'parquet' file?: Could not open Parquet input source 'pretrained_checkpoints/gc_pgp_checkpoint.ckpt': Parquet magic bytes not found in footer. Either the file is corrupted or this is not a parquet file.\n",
+      "2023-11-28 11:03:23,129 INFO {/home/sacardoz/nuplan-devkit/nuplan/planning/nuboard/base/experiment_file_data.py:140}  Could not open Parquet input source '<Buffer>': Parquet magic bytes not found in footer. Either the file is corrupted or this is not a parquet file.\n",
+      "2023-11-28 11:03:23,130 INFO {/home/sacardoz/nuplan-devkit/nuplan/planning/nuboard/base/experiment_file_data.py:140}  Error creating dataset. Could not read schema from 'run_sim_closed_loop/training_raster_experiment/train_default_raster/2023.11.14.22.55.23/hparams.yaml'. Is this a 'parquet' file?: Could not open Parquet input source 'run_sim_closed_loop/training_raster_experiment/train_default_raster/2023.11.14.22.55.23/hparams.yaml': Parquet magic bytes not found in footer. Either the file is corrupted or this is not a parquet file.\n",
+      "2023-11-28 11:03:23,132 INFO {/home/sacardoz/nuplan-devkit/nuplan/planning/nuboard/base/simulation_tile.py:172}  Minimum frame time=0.017 s\n"
+     ]
+    },
+    {
+     "name": "stderr",
+     "output_type": "stream",
+     "text": [
+      "Rendering a scenario: 100%|██████████| 1/1 [00:00<00:00,  7.03it/s]\n",
+      "WARNING:bokeh.core.validation.check:W-1000 (MISSING_RENDERERS): Plot has no renderers: Figure(id='1005', ...)\n",
+      "INFO:tornado.access:200 GET /autoload.js?bokeh-autoload-element=1003&bokeh-absolute-url=http://localhost:8888&resources=none (::1) 959.86ms\n",
+      "INFO:bokeh.server.views.ws:WebSocket connection opened\n",
+      "INFO:tornado.access:101 GET /ws?id=7cadc218-0d10-4ad2-90c5-86e1be250248&origin=da2f890a-eb18-4637-9199-dd0f06169aef&swVersion=4&extensionId=&platform=electron&vscode-resource-base-authority=vscode-resource.vscode-cdn.net&parentOrigin=vscode-file%3A%2F%2Fvscode-app&purpose=notebookRenderer (::1) 0.62ms\n",
+      "INFO:bokeh.server.views.ws:ServerConnection created\n"
+     ]
+    },
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "2023-11-28 11:03:24,076 INFO {/home/sacardoz/miniconda3/envs/nuplan/lib/python3.9/site-packages/tornado/web.py:2344}  200 GET /autoload.js?bokeh-autoload-element=1003&bokeh-absolute-url=http://localhost:8888&resources=none (::1) 959.86ms\n",
+      "2023-11-28 11:03:24,083 INFO {/home/sacardoz/nuplan-devkit/tutorials/utils/tutorial_utils.py:267}  Done rendering!\n",
+      "2023-11-28 11:03:24,084 INFO {/home/sacardoz/miniconda3/envs/nuplan/lib/python3.9/site-packages/tornado/web.py:2344}  101 GET /ws?id=7cadc218-0d10-4ad2-90c5-86e1be250248&origin=da2f890a-eb18-4637-9199-dd0f06169aef&swVersion=4&extensionId=&platform=electron&vscode-resource-base-authority=vscode-resource.vscode-cdn.net&parentOrigin=vscode-file%3A%2F%2Fvscode-app&purpose=notebookRenderer (::1) 0.62ms\n"
+     ]
+    }
+   ],
+   "source": [
+    "from tutorials.utils.tutorial_utils import visualize_history\n",
+    "visualize_history(runner.simulation._history, runner.scenario, bokeh_port=8888)"
+   ]
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "nuplan",
+   "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.9.18"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/nuplan/planning/nuboard/base/plot_data.py b/nuplan/planning/nuboard/base/plot_data.py
index d74f8c66115878f2514b983721fc75add09c9e7c..b61a533e014c74dbdfff16a1a99fbc2a5873f49f 100644
--- a/nuplan/planning/nuboard/base/plot_data.py
+++ b/nuplan/planning/nuboard/base/plot_data.py
@@ -41,6 +41,7 @@ class BokehAgentStates(NamedTuple):
     velocity_ys: List[float]  # [m/s], A list of velocity in y (body frame).
     speeds: List[float]  # [m/s], A list of speed.
     headings: List[float]  # [m], a list of headings
+    occluded: List[bool] # A list of occlusion flags
 
 
 @dataclass(frozen=True)
@@ -451,12 +452,14 @@ class AgentStatePlot(BaseScenarioPlot):
                 for category, data_source in data_sources.items():
                     plot = self.plots.get(category, None)
                     data = dict(data_source.data)
+                    agent_color = simulation_tile_agent_style.get(category)
+                    data['occ_color'] = ['#000000' if occ else agent_color["fill_color"] for occ in data['occluded']]
+
                     if plot is None:
-                        agent_color = simulation_tile_agent_style.get(category)
                         self.plots[category] = main_figure.multi_polygons(
                             xs="xs",
                             ys="ys",
-                            fill_color=agent_color["fill_color"],
+                            fill_color={"field": "occ_color"},
                             fill_alpha=agent_color["fill_alpha"],
                             line_color=agent_color["line_color"],
                             line_width=agent_color["line_width"],
@@ -473,6 +476,7 @@ class AgentStatePlot(BaseScenarioPlot):
                                 ("heading [rad]", "@headings{0.2f}"),
                                 ("type", "@agent_type"),
                                 ("track token", "@track_token"),
+                                ("occluded", "@occluded"),
                             ],
                         )
                         main_figure.add_tools(agent_hover)
@@ -499,6 +503,7 @@ class AgentStatePlot(BaseScenarioPlot):
 
                 tracked_objects = sample.observation.tracked_objects
                 frame_dict = {}
+                time_us = sample.iteration.time_us
                 for tracked_object_type_name, tracked_object_type in tracked_object_types.items():
                     corner_xs = []
                     corner_ys = []
@@ -511,6 +516,7 @@ class AgentStatePlot(BaseScenarioPlot):
                     velocity_ys = []
                     speeds = []
                     headings = []
+                    occluded = []
 
                     for tracked_object in tracked_objects.get_tracked_objects_of_type(tracked_object_type):
                         agent_corners = tracked_object.box.all_corners()
@@ -530,6 +536,11 @@ class AgentStatePlot(BaseScenarioPlot):
                         track_ids.append(self._get_track_id(tracked_object.track_token))
                         track_tokens.append(tracked_object.track_token)
 
+                        if history.occlusion_masks is not None:
+                            occluded.append(tracked_object.track_token not in history.occlusion_masks[time_us])
+                        else:
+                            occluded.append(False)
+
                     agent_states = BokehAgentStates(
                         xs=corner_xs,
                         ys=corner_ys,
@@ -542,6 +553,7 @@ class AgentStatePlot(BaseScenarioPlot):
                         velocity_ys=velocity_ys,
                         speeds=speeds,
                         headings=headings,
+                        occluded=occluded,
                     )
 
                     frame_dict[tracked_object_type_name] = ColumnDataSource(agent_states._asdict())
diff --git a/nuplan/planning/script/builders/occlusion_manager_builder.py b/nuplan/planning/script/builders/occlusion_manager_builder.py
new file mode 100644
index 0000000000000000000000000000000000000000..2b75cc59d7d4c7b922ced2a140268924e04f6d21
--- /dev/null
+++ b/nuplan/planning/script/builders/occlusion_manager_builder.py
@@ -0,0 +1,18 @@
+
+from omegaconf import DictConfig
+
+from nuplan.planning.scenario_builder.abstract_scenario import AbstractScenario
+from nuplan.planning.simulation.occlusion.abstract_occlusion_manager import AbstractOcclusionManager
+from nuplan.planning.simulation.occlusion.range_occlusion_manager import RangeOcclusionManager
+
+def build_occlusion_manager(occlusion_cfg: DictConfig, scenario: AbstractScenario) -> AbstractOcclusionManager:
+    """
+    Instantiate occlusion_manager
+    :param occlusion_cfg: config of a occlusion_manager
+    :param scenario: scenario
+    :return occlusion_cfg
+    """
+    # Placeholder
+    occlusion_manager: AbstractOcclusionManager = RangeOcclusionManager(scenario)
+
+    return occlusion_manager
diff --git a/nuplan/planning/script/builders/simulation_builder.py b/nuplan/planning/script/builders/simulation_builder.py
index 1d35ab7af41cb7c488053277a72ac661b8a47cd7..52abd74a11c3fc7fc3104946adc9b33a3da52f44 100644
--- a/nuplan/planning/script/builders/simulation_builder.py
+++ b/nuplan/planning/script/builders/simulation_builder.py
@@ -9,6 +9,7 @@ from nuplan.common.utils.distributed_scenario_filter import DistributedMode, Dis
 from nuplan.planning.scenario_builder.nuplan_db.nuplan_scenario_builder import NuPlanScenarioBuilder
 from nuplan.planning.script.builders.metric_builder import build_metrics_engines
 from nuplan.planning.script.builders.observation_builder import build_observations
+from nuplan.planning.script.builders.occlusion_manager_builder import build_occlusion_manager
 from nuplan.planning.script.builders.planner_builder import build_planners
 from nuplan.planning.script.builders.utils.utils_type import is_target_type
 from nuplan.planning.simulation.callback.abstract_callback import AbstractCallback
@@ -16,6 +17,7 @@ from nuplan.planning.simulation.callback.metric_callback import MetricCallback
 from nuplan.planning.simulation.callback.multi_callback import MultiCallback
 from nuplan.planning.simulation.controller.abstract_controller import AbstractEgoController
 from nuplan.planning.simulation.observation.abstract_observation import AbstractObservation
+from nuplan.planning.simulation.occlusion.abstract_occlusion_manager import AbstractOcclusionManager
 from nuplan.planning.simulation.planner.abstract_planner import AbstractPlanner
 from nuplan.planning.simulation.runner.simulations_runner import SimulationRunner
 from nuplan.planning.simulation.simulation import Simulation
@@ -103,6 +105,12 @@ def build_simulations(
             # Perception
             observations: AbstractObservation = build_observations(cfg.observation, scenario=scenario)
 
+            # Occlusions
+            if 'occlusion' in cfg.keys() and cfg.occlusion:
+                occlusion_manager: AbstractOcclusionManager = build_occlusion_manager(cfg.occlusion, scenario=scenario)
+            else:
+                occlusion_manager = None
+
             # Metric Engine
             metric_engine = metric_engines_map.get(scenario.scenario_type, None)
             if metric_engine is not None:
@@ -120,6 +128,7 @@ def build_simulations(
                 time_controller=simulation_time_controller,
                 observations=observations,
                 ego_controller=ego_controller,
+                occlusion_manager=occlusion_manager,
                 scenario=scenario,
             )
 
diff --git a/nuplan/planning/script/run_simulation.py b/nuplan/planning/script/run_simulation.py
index d97f24611440b72d59a5cf7db563118f45c9b6ab..9e3e5cff00c21868b1afcc9dc35d52371cce640f 100644
--- a/nuplan/planning/script/run_simulation.py
+++ b/nuplan/planning/script/run_simulation.py
@@ -34,6 +34,42 @@ if os.path.basename(CONFIG_PATH) != 'simulation':
 CONFIG_NAME = 'default_simulation'
 
 
+def build_simulation_runners(cfg: DictConfig, planners: Optional[Union[AbstractPlanner, List[AbstractPlanner]]] = None):
+    # Fix random seed
+    pl.seed_everything(cfg.seed, workers=True)
+
+    profiler_name = 'building_simulation'
+    common_builder = set_up_common_builder(cfg=cfg, profiler_name=profiler_name)
+
+    # Build simulation callbacks
+    callbacks_worker_pool = build_callbacks_worker(cfg)
+    callbacks = build_simulation_callbacks(cfg=cfg, output_dir=common_builder.output_dir, worker=callbacks_worker_pool)
+
+    # Remove planner from config to make sure run_simulation does not receive multiple planner specifications.
+    if planners and 'planner' in cfg.keys():
+        logger.info('Using pre-instantiated planner. Ignoring planner in config')
+        OmegaConf.set_struct(cfg, False)
+        cfg.pop('planner')
+        OmegaConf.set_struct(cfg, True)
+
+    # Construct simulations
+    if isinstance(planners, AbstractPlanner):
+        planners = [planners]
+
+    runners = build_simulations(
+        cfg=cfg,
+        callbacks=callbacks,
+        worker=common_builder.worker,
+        pre_built_planners=planners,
+        callbacks_worker=callbacks_worker_pool,
+    )
+
+    if common_builder.profiler:
+        # Stop simulation construction profiling
+        common_builder.profiler.save_profiler(profiler_name)
+
+    return runners, common_builder, cfg
+
 def run_simulation(cfg: DictConfig, planners: Optional[Union[AbstractPlanner, List[AbstractPlanner]]] = None) -> None:
     """
     Execute all available challenges simultaneously on the same scenario. Helper function for main to allow planner to
diff --git a/nuplan/planning/simulation/history/simulation_history.py b/nuplan/planning/simulation/history/simulation_history.py
index 65bd30a98c8a0b52e23d11b0e9c4e1ddda0db54f..d2b7042de5b7e0085303990ae03ab4005f3160f8 100644
--- a/nuplan/planning/simulation/history/simulation_history.py
+++ b/nuplan/planning/simulation/history/simulation_history.py
@@ -41,6 +41,8 @@ class SimulationHistory:
         """
         self.map_api: AbstractMap = map_api
         self.mission_goal = mission_goal
+        # NOTE: This is just for visualization code, not used during simulation
+        self.occlusion_masks = None
 
         self.data: List[SimulationHistorySample] = data if data is not None else list()
 
diff --git a/nuplan/planning/simulation/occlusion/abstract_occlusion_manager.py b/nuplan/planning/simulation/occlusion/abstract_occlusion_manager.py
new file mode 100644
index 0000000000000000000000000000000000000000..654c0af62185ff9a145783ba46160c8ecd393abd
--- /dev/null
+++ b/nuplan/planning/simulation/occlusion/abstract_occlusion_manager.py
@@ -0,0 +1,74 @@
+from abc import ABCMeta, abstractmethod
+from collections import deque
+from typing import Tuple
+
+from nuplan.common.actor_state.ego_state import EgoState
+from nuplan.common.actor_state.tracked_objects import TrackedObjects
+
+from nuplan.planning.scenario_builder.abstract_scenario import AbstractScenario
+from nuplan.planning.simulation.history.simulation_history_buffer import SimulationHistoryBuffer
+from nuplan.planning.simulation.observation.observation_type import DetectionsTracks, Observation
+
+
+class AbstractOcclusionManager(metaclass=ABCMeta):
+    """
+    Interface for a generic occlusion manager.
+    """
+
+    def __init__(
+        self,
+        scenario: AbstractScenario
+    ):
+        self._visible_agent_cache = {}
+        self.scenario = scenario
+
+    def reset(self) -> None:
+        """
+        Resets occlusion manager cache.
+        """
+        self._visible_agent_cache = {}
+
+    def occlude_input(self, input_buffer: SimulationHistoryBuffer) -> SimulationHistoryBuffer:
+        """
+        Occludes SimulationHistoryBuffer input. Loops through each timestep defined by time_us,
+        checks to see if timestep is already contained in _visible_agent_cache and computes
+        occlusions if not, and occludes timestep using cached results. Repacks output in 
+        SimulationHistoryBuffer.
+        """
+
+        ego_state_buffer = input_buffer.ego_state_buffer
+        observations_buffer = input_buffer.observation_buffer
+        sample_interval = input_buffer.sample_interval
+
+        for ego_state, observations in zip(ego_state_buffer, observations_buffer):
+            if ego_state.time_us not in self._visible_agent_cache:
+                self._visible_agent_cache[ego_state.time_us] = self._compute_visible_agents(ego_state, observations)
+                
+        output_buffer = SimulationHistoryBuffer(ego_state_buffer, \
+                            deque([self._mask_input(ego_state.time_us, observations) for ego_state, observations in zip(ego_state_buffer, observations_buffer)]), \
+                                sample_interval)
+
+        return output_buffer
+    
+    @abstractmethod
+    def _compute_visible_agents(self, ego_state: EgoState, observations: DetectionsTracks) -> set:
+        """
+        Returns set of track tokens that represents the observations visible to the ego
+        at this time step.
+        """
+        pass
+
+    def _mask_input(self, time_us: int, observations: DetectionsTracks) -> DetectionsTracks:
+        """
+        Occludes observations at timestep time_us based on cached occlusions.
+        """
+
+        assert time_us in self._visible_agent_cache, "Attempted to mask non-cached timestep!"
+        assert isinstance(observations, DetectionsTracks), "Occlusions only support DetectionsTracks."
+
+        mask = self._visible_agent_cache[time_us]
+        tracks = observations.tracked_objects.tracked_objects
+
+        visible_tracks = [track for track in tracks if track.metadata.track_token in mask]
+
+        return DetectionsTracks(tracked_objects=TrackedObjects(visible_tracks))
diff --git a/nuplan/planning/simulation/occlusion/range_occlusion_manager.py b/nuplan/planning/simulation/occlusion/range_occlusion_manager.py
new file mode 100644
index 0000000000000000000000000000000000000000..9d513fb42fb818206b339a25739788a14b1276bf
--- /dev/null
+++ b/nuplan/planning/simulation/occlusion/range_occlusion_manager.py
@@ -0,0 +1,38 @@
+from nuplan.common.actor_state.ego_state import EgoState
+from nuplan.planning.scenario_builder.abstract_scenario import AbstractScenario
+from nuplan.planning.simulation.observation.observation_type import DetectionsTracks
+from nuplan.planning.simulation.occlusion.abstract_occlusion_manager import AbstractOcclusionManager
+
+
+class RangeOcclusionManager(AbstractOcclusionManager):
+    """
+    Range occlusion manager. Occludes all objects outside of a given
+    range of the ego.
+    """
+
+    def __init__(
+        self,
+        scenario: AbstractScenario,
+        range_threshold: float = 25
+    ):
+        super().__init__(scenario)
+        self.range_threshold = range_threshold
+
+    def _compute_visible_agents(self, ego_state: EgoState, observations: DetectionsTracks) -> set:
+        """
+        Returns set of track tokens that represents the observations visible to the ego
+        at this time step.
+        """
+
+        # Visible track token set
+        not_occluded = set()
+
+        # Loop through observations and check if it's closer to the ego then range_threshold, 
+        # add to output set if so.
+        for track in observations.tracked_objects.tracked_objects:
+            if ((ego_state.center.x - track.center.x) ** 2 + \
+                (ego_state.center.y - track.center.y) ** 2) ** 0.5 <= self.range_threshold:
+                not_occluded.add(track.metadata.track_token)
+
+        return not_occluded
+    
\ No newline at end of file
diff --git a/nuplan/planning/simulation/simulation.py b/nuplan/planning/simulation/simulation.py
index 243d8d345c42c86e1f8be18497185b26329c0c55..dda8c01b87aecbdb09bb6e3f193a04742a46b0cd 100644
--- a/nuplan/planning/simulation/simulation.py
+++ b/nuplan/planning/simulation/simulation.py
@@ -45,6 +45,7 @@ class Simulation:
         self._time_controller = simulation_setup.time_controller
         self._ego_controller = simulation_setup.ego_controller
         self._observations = simulation_setup.observations
+        self._occlusion_manager = simulation_setup.occlusion_manager
         self._scenario = simulation_setup.scenario
         self._callback = MultiCallback([]) if callback is None else callback
 
@@ -136,8 +137,13 @@ class Simulation:
 
         # Extract traffic light status data
         traffic_light_data = list(self._scenario.get_traffic_light_status_at_iteration(iteration.index))
+
+        history_input = self._history_buffer
+        if self._occlusion_manager is not None:
+            history_input = self._occlusion_manager.occlude_input(history_input)
+
         logger.debug(f"Executing {iteration.index}!")
-        return PlannerInput(iteration=iteration, history=self._history_buffer, traffic_light_data=traffic_light_data)
+        return PlannerInput(iteration=iteration, history=history_input, traffic_light_data=traffic_light_data)
 
     def propagate(self, trajectory: AbstractTrajectory) -> None:
         """
@@ -162,6 +168,9 @@ class Simulation:
         self._history.add_sample(
             SimulationHistorySample(iteration, ego_state, trajectory, observation, traffic_light_status)
         )
+        
+        if self._occlusion_manager:
+            self._history.occlusion_masks = self._occlusion_manager._visible_agent_cache
 
         # Propagate state to next iteration
         next_iteration = self._time_controller.next_iteration()
diff --git a/nuplan/planning/simulation/simulation_setup.py b/nuplan/planning/simulation/simulation_setup.py
index 34da9d3627e2b41735a6fed6e3767def17b02b41..03b3d39eefef9ce0131d6719efb46f909d2f416d 100644
--- a/nuplan/planning/simulation/simulation_setup.py
+++ b/nuplan/planning/simulation/simulation_setup.py
@@ -3,6 +3,7 @@ from dataclasses import dataclass
 from nuplan.planning.scenario_builder.abstract_scenario import AbstractScenario
 from nuplan.planning.simulation.controller.abstract_controller import AbstractEgoController
 from nuplan.planning.simulation.observation.abstract_observation import AbstractObservation
+from nuplan.planning.simulation.occlusion.abstract_occlusion_manager import AbstractOcclusionManager
 from nuplan.planning.simulation.planner.abstract_planner import AbstractPlanner
 from nuplan.planning.simulation.simulation_time_controller.abstract_simulation_time_controller import (
     AbstractSimulationTimeController,
@@ -16,6 +17,7 @@ class SimulationSetup:
     time_controller: AbstractSimulationTimeController
     observations: AbstractObservation
     ego_controller: AbstractEgoController
+    occlusion_manager: AbstractOcclusionManager
     scenario: AbstractScenario
 
     def __post_init__(self) -> None:
@@ -30,7 +32,7 @@ class SimulationSetup:
         assert isinstance(
             self.ego_controller, AbstractEgoController
         ), 'Error: ego_controller must inherit from AbstractEgoController!'
-
+        
     def reset(self) -> None:
         """
         Reset all simulation controllers
@@ -39,6 +41,9 @@ class SimulationSetup:
         self.ego_controller.reset()
         self.time_controller.reset()
 
+        if self.occlusion_manager:
+            self.occlusion_manager.reset()
+
 
 def validate_planner_setup(setup: SimulationSetup, planner: AbstractPlanner) -> None:
     """
diff --git a/tutorials/utils/tutorial_utils.py b/tutorials/utils/tutorial_utils.py
index 714b7cd2589252bcead07f7284e0c966be4f65f3..2406af9943d3a0b26e7233ca7de359168f377ae0 100644
--- a/tutorials/utils/tutorial_utils.py
+++ b/tutorials/utils/tutorial_utils.py
@@ -213,6 +213,23 @@ def serialize_scenario(
     return simulation_history
 
 
+
+def visualize_history(
+    simulation_history: SimulationHistory, scenario: NuPlanScenario, save_dir: str = '/tmp/scenario_visualization/', bokeh_port: int = 8899
+) -> None:
+    """
+    Visualize a scenario in Bokeh.
+    :param scenario: Scenario object to be visualized.
+    :param save_dir: Dir to save serialization and visualization artifacts.
+    :param bokeh_port: Port that the server bokeh starts to render the generate the visualization will run on.
+    """
+    map_factory = NuPlanMapFactory(get_maps_db(map_root=scenario.map_root, map_version=scenario.map_version))
+
+    simulation_scenario_key = save_scenes_to_dir(
+        scenario=scenario, save_dir=save_dir, simulation_history=simulation_history
+    )
+    visualize_scenarios([simulation_scenario_key], map_factory, Path(save_dir), bokeh_port=bokeh_port)
+
 def visualize_scenario(
     scenario: NuPlanScenario, save_dir: str = '/tmp/scenario_visualization/', bokeh_port: int = 8899
 ) -> None: