Repository: skhatiri/Aerialist (language: python)

Issue:
Docker desktop k8s
Updated Code to enable local testing using Kubernetes with Docker Desktop


Discussion / hints:

Fix patch:
diff --git a/.gitignore b/.gitignore
index 6336e50..7e74cef 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,7 +1,6 @@
 logs/
-tmp/**
+tmp/
 .env
-k8s-config.yaml
 venv/
 results/
 samples/flights/*_0.csv
@@ -50,4 +49,6 @@ debug*.log
 debugpy*.log
 pydevd*.log
 nodeLanguageServer/**
-nodeLanguageServer.*/**
\ No newline at end of file
+nodeLanguageServer.*/**
+.idea/
+k8s-config.yaml
diff --git a/Dockerfile b/Dockerfile
index 223940a..2e7b2b5 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -16,7 +16,7 @@ RUN pip3 install -r requirements.txt
 COPY . .
 RUN chmod +x ./aerialist/__main__.py
 COPY ./template.env ./.env
-RUN mkdir -p /io/ ./results/logs/
+RUN mkdir -p /io/ ./results/logs/ tmp/
 
 # ENTRYPOINT [ "./run.py" ]
 # CMD [ "--help" ]
diff --git a/LICENSE.md b/LICENSE.md
index 43b256d..83c2bda 100644
--- a/LICENSE.md
+++ b/LICENSE.md
@@ -1,6 +1,6 @@
 MIT License
 
-Copyright (c) 2023 Sajad MazraehKhatiri, Sebastiano Panichella, and Paolo Tonella
+Copyright (c) 2023 Sajad Mazraeh Khatiri
 
 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
 
diff --git a/README.md b/README.md
index cb5730c..4d9cda6 100644
--- a/README.md
+++ b/README.md
@@ -1,94 +1,191 @@
 # AERIALIST: UAV Test Bench
 
-## [Demo Video](https://youtu.be/pmBspS2EiGg)
+<!-- ## [Demo Video](https://youtu.be/pmBspS2EiGg) -->
+
+**Aerialist** (unmanned AERIAL vehIcle teST bench) is a novel test bench for UAV software that automates all the necessary UAV testing steps: setting up the test environment, building and running the UAV firmware code, configuring the simulator with the simulated world properties, connecting the simulated UAV to the firmware and applying proper UAV configurations at startup, scheduling and executing runtime commands, monitoring the UAV at runtime for any issues, and extracting the flight log file after the test completion.
+
+With Aerialist, we aim to provide researchers with an easy platform to automate tests on both simulated and real UAVs, allowing them to do experiments required to overcome the UAV simulation-based testing challenges.
+
+## Table of Contents
+
+- [Introduction](#introduction)
+  - [UAV Tests](#uav-tests)
+  - [Demo Video](#demo-video)
+- [Getting Started](#getting-started)
+  - [Docker Test Execution](#docker-test-execution)
+  - [Local Test Execution](#local-test-execution)
+  - [Kubernetes Test Execution](#kubernetes-test-execution)
+- [Usage](#usage)
+  - [Test Description File](#test-description-file)
+  - [Command Line Interface](#command-line-interface)
+  - [Python API](#using-aerialist-in-your-code)
+- [References](#references)
+<!-- - [Contributing](#contributing) -->
+- [License](#license)
+- [Contacts](#contacts)
+
+## Introduction
+<!-- **Aerialist** (unmanned AERIAL vehIcle teST bench) is a modular and extensible test bench for UAV software and it aims to facilitate and automate all the necessary steps of definition, generation, execution, and analysis of system-level test cases for UAVs. -->
+The below figure demonstrates Aerialist's software architecture, with the current implementation supporting UAVs powered by [PX4-Autopilot](https://github.com/PX4/PX4-Autopilot) (a widely used open-source UAV firmware).
+
+The input is a [**Test Description**](#test-description-file) file, which defines the UAV and environment configurations and the test steps.
+The **Test Runner** subsystem (that abstracts any dependencies to the actual UAV, its software platform, and the simulation environment) prepares the environment for running the test case as described in the test description.  
+After setting up the simulation environment (if testing a simulated UAV), the Test Runner connects to the (simulated or physical) UAV and configures it according to the startup instructions. Then, it sends runtime commands, monitors the UAV's state during the flight, and extracts flight logs at the end of the test for future analysis. Each module is detailed in the [Architecture Documents](docs/architecture.md).
 
-Aerialist (unmanned AERIAL vehIcle teST bench) is a modular and extensible test bench for UAV software, supporting both simulated and physical UAVs.
+![Aerialist Architecture](docs/architecture.png)
 
-In Aerialist, the tests are defined as specific **software configurations** in a given **simulated/physical environment** and a set of **runtime commands** that make the UAV fly with a specific **observable behavior** (e.g., flight trajectory, speed, distance to obstacles).
-We model a UAV simulated test case with the following test properties:
+### UAV Tests
 
-- UAV Configuration: [Autopilot parameters](https://docs.px4.io/main/en/advanced_config/parameter_reference.html) set at startup, configuration files (e.g., mission plan) required, etc.
-- Environment Configuration: Simulation settings such as simulation world (e.g., surface material, UAV’s initial position), surrounding objects (e.g., obstacles size, position), weather condition (e.g., wind, lighting), etc.
-- Runtime Commands: Timestamped external commands sent from GCS or RC to the UAV during the flight (e.g., changing the flight mode, flying in a specific direction, starting autonomous flight)
-- Expectation (optional): time series of certain sensor reading that the test flights are expected to follow closely.
+The de-facto testing standard of UAVs relies on *manually written system-level tests* to test UAVs *in the field*.
+These tests are defined as **software configurations** (using parameters, config files, etc.) in a specific **environment** setup (e.g., obstacles placement, lighting conditions) and a set of runtime **commands**.
+The runtime commands received during the UAV flight (e.g., from a remote controller) make the UAV fly with a specific human observable **behavior** (e.g., trajectory, speed, distance to obstacles).
 
-The test definition can be in a config file, set in specific environment variables, or provided directly to the Command Line Interface (CLI) as parameters.
+Hence, Aerialist models a UAV test case with the following set of *test properties* and uses a *YAML* structure to describe the test.
 
-Aerialist prepares the environment for running the tests and abstracts any dependencies to the actual UAV, its software platform, specific flight modes and simulation environment. After setting up the simulation environment as described in the test description (if testing a simulated UAV), It connects to the drone (simulated or physical) and configures it as instructed at startup, and starts sending runtime commands. It also monitors UAV state during the flight, and extracts the flight logs at the end of the test for future analysis.
+- **Drone**: Software configurations of the UAV model, including all [Autopilot parameters](https://docs.px4.io/main/en/advanced_config/parameter_reference.html) and configuration files (e.g., mission plan) required to set up the drone for the test.
 
-Below figure demonstrates the overall architecture of Aerialist and it is described in mored details in the [later sections](#software-architecture).
-The implementation currently supports [PX4-Autopilot](https://github.com/PX4/PX4-Autopilot), but can be extended to support other UAV platforms as well.
+- **Environment**: Simulation settings such as the used simulator, physics of the simulated UAV, simulation world (e.g., surface material, UAV’s initial position), surrounding objects (e.g., obstacles size, position), weather conditions (e.g., wind, lighting), etc.
 
-![Aerialist Architecture](docs/architecture.png)
+- **Commands**: Timestamped external commands from the ground control station (GCS) or the remote controller (RC) to the UAV during the flight (e.g., change  flight mode, go in a specific direction, enter mission mode).
+
+- **Expectation (optional)**: a time series of certain sensor readings that the test flights are expected to follow closely.
+
+### Demo Video
+
+Take a look at the [Demo Video](https://youtu.be/cIeUN8f00L0) to get more insights on the test execution methods introduced later.
+
+[![IMAGE ALT TEXT HERE](https://img.youtube.com/vi/cIeUN8f00L0/0.jpg)](https://www.youtube.com/watch?v=cIeUN8f00L0)
+
+## Getting Started
+
+You can execute UAV test cases with Aerialist in three different ways.
+
+- [Docker Test Execution](#docker-test-execution) (**Recommended**): Execute Test Cases in pre-built Docker containers without the need to install PX4 dependencies on your machine.
+This is the recommended option for most use cases and supports headless simulation (without the graphical interface).
+
+- [Local Test Execution](#local-test-execution): Execute Test Cases using PX4 dependencies installed on the same machine.
+This allows you to easily develop new functionalities and run the UAV simulators with the graphical interface so you can visually follow the UAV behavior.
+This is only recommended if you prefer to get more insights into UAV behavior.
+
+- [Kubernetes Test Execution](#kubernetes-test-execution): You can also deploy your test execution to a Kubernetes cluster for more scale.
+This option is only recommended if you are using Aerialist to conduct large-scale experiments on test generation for drones.
+
+### Docker Test Execution
+
+Using Docker containers with pre-configured PX4 dependencies to execute test cases is the simplest and recommended way of executing UAV tests with Aerialist.
+Aerialist's Docker image is hosted on [Dockerhub](https://hub.docker.com/r/skhatiri/aerialist).
+
+#### Using Docker Container's CLI
+
+- Requirements: [Docker](https://docs.docker.com/engine/install/)
+- This has been tested on **Windows, Ubuntu and macOS with x86-64 processors**.
+  - You may need to rebuild the docker image if you are using another OS or architecture.
+
+1. `docker run -it skhatiri/aerialist bash`
+
+- You can now use the [Command Line Interface](#command-line-interface) in the container's bash.
+- check `python3 aerialist exec --help`
+
+**Note:** The .env for the docker image comes from [template.env](template.env). You can customize them using [environment variables](https://docs.docker.com/engine/reference/commandline/run/#env) for the Docker container.
+
+#### Using Host's CLI
+
+Alternatively, you can use the CLI on your local machine and instruct Aerialist to execute test cases inside a docker container.
+This gives you more flexibility since the test results (flight logs) are directly stored on the host machine and you don't lose them.
 
-## Setup
+- Requirements:
+  - [Docker](https://docs.docker.com/engine/install/)
+  - Python ≥ 3.8
+  - Your user should be able to run docker commands without `sudo`. [check here](https://docs.docker.com/engine/install/linux-postinstall/)
+- This has been tested on **Ubuntu and macOS with x86-64 processors**.
+  - You may need to rebuild the docker image if you are using another OS or architecture.
 
-You can setup Aerialist in different ways:
+1. Clone this repository and `cd` into its root directory
+2. `pip3 install -r requiremetns.txt`
+3. Create a file named `.env` in the repository's root directory. Then copy and customize the contents of [`template.env`](template.env) into it.
+4. Create a folder named `results` in the repository's root directory.
+5. You can use the dockerfile to build a Docker image with all the requirements, or instead pull the latest image from the Image repository.
 
-- [Locally on your Ubuntu machine](#local-development-environment), which gives you the opportunity to easily develop new functionalities, and run the UAV simulators with the graphical interface, so you can also visually follow the UAV behavior.
+- `docker build . -t skhatiri/aerialist`
+- or `docker pull skhatiri/aerialist`
 
-- [Locally within a Docker container](#using-docker), which automates all the technical requiremens and the setup process, and lets you easily run tests with headless simulation (without the graphical interface)
+6. You can now instruct Aerialist to execute test cases inside a docker container
 
-- [On a Kubernetes Cluster in the cloud](#using-k8s), which makes it very easy to run large scale experiments, speciffically when multiple executions of each test case is needed for eliminating randomness in the UAV behaviour.
+- Just add `--agent docker` to the command line or update the test-description file (`agent.engine:docker`).
+- You can now use the [Command Line Interface](#command-line-interface) in your local bash.
+  - check `python3 aerialist exec --help`
 
-- [As a python package](#using-pip) to integrate its functionalities in your own code.
+### Local Test Execution
 
-### Local Development Environment
+**Note:** Installing [PX4-Autopilot](https://github.com/PX4/PX4-Autopilot), [PX4-Avoidance](https://github.com/PX4/PX4-Avoidance) and their requirements including ROS and Gazebo could be problematic in many cases. We only suggest this full installation to users who need direct visual access to the simulator or are curious to visually monitor the UAVs during the flight. Otherwise, test execution, extracting the flight logs, and plotting them can be achieved by the [docker exection](#docker-test-execution) as well.
 
-Aerialist requires python >= 3.8 and has been tested with PX4 development environment on ubuntu 18.04 and 20.04
+- We have prepared a ready-to-use virtual machine ([download link](https://zhaw-my.sharepoint.com/:f:/g/personal/mazr_zhaw_ch/EnxLqlyju6RMhUYV_SXTqBEBfxundq_-X67eRQAwCPjHvg?e=9953JZ)) to help users onboaord fast. You can move on to using [dockerized test executions](#docker-test-execution) if you don't need the simulation visualizations any more.
+
+If you prefer to run the simulations and PX4 on your own machine, follow [PX4 installation guide](./docs/PX4_installation.md).
+
+- Requirements:
+  - **Ubuntu 18**
+
+1. We have prepared a [bashsript](./setup_script/full_setup.sh) to automate all the steps of installing PX4 and Aerialist in one shot. [Follow the instructions](./docs/PX4_installation.md#instalation-using-bash-script).
+
+- You can also follow a [step-by-step guide](./docs/PX4_installation.md#step-by-step-instlation) if needed.
 
-1. Setup PX4 development environment. Follow the instrudctions [here](https://docs.px4.io/master/en/dev_setup/dev_env_linux_ubuntu.html)
 2. Clone this repository and cd into its root directory
 3. `pip3 install -r requiremetns.txt`
 4. Create a file named `.env` in the repository's root directory. Then copy and customize the contents of [`template.env`](template.env) into it.
 
-**Note:** Update *PX4_HOME* and *RESULTS_DIR* variables according to your installation.
+- Update *PX4_HOME* and *RESULTS_DIR* variables according to your installation.
 
-### Using Docker
+5. You can now use the [Command Line Interface](#command-line-interface).
 
-You can use the dockerfile to build a Docker image with all the requirements, or instead pull the latest image from the Image repository.
+### Kubernetes Test Execution
 
-1. `docker build . -t aerialist` or `docker pull skhatiri/aerialist`
-2. `docker run -it aerialist bash`
+Aerialist can also deploy test executions on a Kubernetes cluster to facilitate running tests in the cloud. Specifically, as can be seen in the below figure, Aerialist can run multiple executions of the same test case in isolated Kubernets pods in parallel, and gather test results for further processing.
 
-You can now execute all the following commands in the containers bash.
+This feature is specifically helpful when performing test generation tasks, where UAV's flight could be subject to non-determinism and multiple simulations are required.
+Follow our [K8S Deployment Guideline](./docs/k8s_setup.md).  
 
-**Note:** Your user should be able to run docker commands without sudo. [check here](https://docs.docker.com/engine/install/linux-postinstall/)
-**Note:** The .env for the docker image come from [template.env](template.env). You can customize them using [environment variables](https://docs.docker.com/engine/reference/commandline/run/#env) for the Docker container.
+<!-- 
+<p align="center"><img src="docs/deployment.png" alt="Kubernetes Deployment" width="60%"/></p>
+Aerialist can connect both to a cloud Kubernetes cluster, or a local instance (more useful during development).
 
-### Using K8S
+- Requirements:
+  - [kubectl](https://kubernetes.io/docs/tasks/tools/#kubectl)
+    - [Set default context and namespace](https://kubernetes.io/docs/reference/kubectl/cheatsheet/#kubectl-context-and-configuration) to your prefered clster and namespace if needed
+  - [yq](https://github.com/mikefarah/yq#install)
 
-Aerialist can be easily depoyed on a Kubernetes cluster to facilitate running tests in the cloud. Speciffically, as can be seen in the below figure, Aerialist can run multiple executions of the same test case in isolated Kubernets pods in parallel, and gather test results for further processing.
+      ```bash
+      wget https://github.com/mikefarah/yq/releases/download/v4.22.1/yq_linux_amd64 -O /usr/bin/yq &&\
+          chmod +x /usr/bin/yq
+      ```
 
-![Kubernetes Deployment](docs/deployment.png)
+#### Using Local Kubernetes Instance
 
-Aerialist uses a [NextCloud](https://nextcloud.com/) instance to share files between the main container, and the parallel test executers. You can get a free account in [a cloud provider](https://nextcloud.com/sign-up/) or deploy your own [dockerized instance](https://hub.docker.com/_/nextcloud).
+**TODO**
 
-1. Set your NextCloud credentials and address in as a k8s-Secret: `kubectl create secret generic webdav --from-literal=host=https://[your-nextcloud-address]/remote.php/dav/files/[your-account-id]/ --from-literal=root=https://[your-nextcloud-address]/remote.php/webdav/ --from-literal=user=[username] --from-literal=pass=[password]`
-2. Upload your [`k8s-config.yaml`](https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/) as a k8s-ConfigMap: `kubectl create configmap k8s-config --from-file k8s-config.yaml`
-3. You can now use `--k8s` in the commands to run the simulations in your k8s-cluster.
-`python3 aerialist exec --id case0-manual --obstacle 1 1 1 -8.1 3.1 0 0 --path https://[your-nextcloud-address]/remote.php/webdav/ --mission samples/flights/mission1.plan --log samples/flights/mission1.ulg --params samples/flights/mission1-params.csv --commands samples/flights/mission1-commands.csv --drone ros --simulator ros --agent k8s -n 5`
+#### Using Cloud Kubernetes Cluster
 
-### Using Pip
+Aerialist uses a [NextCloud](https://nextcloud.com/) instance to share files between the main container, and the parallel test executers. You can get a free account in [a cloud provider](https://nextcloud.com/sign-up/) or deploy your own [dockerized instance](https://hub.docker.com/_/nextcloud).
 
-1. `pip3 install git+https://github.com/skhatiri/Aerialist.git`
+1. Set your NextCloud credentials and address in as a k8s-Secret:
+  `kubectl create secret generic webdav --from-literal=host=https://[your-nextcloud-address]/remote.php/dav/files/[your-account-id]/ --from-literal=user=[username] --from-literal=pass=[password]`
 
-## Command-Line Interface
+2. Upload your [`k8s-config.yaml`](https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/) as a k8s-ConfigMap:
+  `kubectl create configmap k8s-config --from-file k8s-config.yaml`
 
-You can utilize the toolkit with following command line options:
+3. You can now use `--agent k8s` in the commands to run the simulations in your k8s-cluster.
+  `python3 aerialist exec --test samples/tests/mission1.yaml --agent k8s -n 5 --id mission-test --path webdav://` -->
 
-**Note:** Before running any command, make sure you are at the root directory of the repository:
+## Usage
 
-`cd Aerialist/`
+### Test Description File
 
 Using a predefined [test-description yaml file](samples/tests/template-test.yaml) is the easiest way to define the test case.
 
-`python3 aerialist exec --test samples/tests/manual1-local.yaml`
-
 ```yaml
 # template-test.yaml
 drone:
-  port: sitl # type of the drone to conect to {sitl, ros, cf}
+  port: sitl # type of the drone to connect to {sitl, ros, cf}
   #params: #PX4 parameters : https://docs.px4.io/main/en/advanced_config/parameter_reference.html
     # {parameter_name}: {parameter_value} #(keep datatype -> e.g, 1.0 for float, 1 for int)
     # CP_DIST: 1.0
@@ -100,106 +197,79 @@ simulation:
   simulator: ros # the simulator environment to run {gazebo,jmavsim,ros} 
   speed: 1 # the simulator speed relative to real time
   headless: true # whether to run the simulator headless
-  obstacles: [5,5,5,  10,5,0,  0] # propetries (size, position, and rotation) of box shaped obstaclesto put in simulation environment: [l,w,h,x,y,z,r]
+  obstacles:
+  - size: # Object 1 size in l,w,h
+      l: 5
+      w: 5
+      h: 5
+    position: # Object 1 position in x,y,z and it's rotation
+      x: -8
+      y: 3
+      z: 0
+      r: 0
   # home_position: # home position to place the drone [lat,lon,alt]  
 test:
   commands_file: samples/flights/mission1-commands.csv # runtime commands file address
-  speed: 1 # the commands speed relative to real time
 
 assertion:
   log_file: samples/flights/mission1.ulg # reference log file address
   # variable: trajectory # reference variables to compare 
 
-agent:
-  engine: k8s # where to run the tests {k8s, docker, local}
-  count: 5 # no. of parallel runs (only for k8s)
-  path: https://filer.cloudlab.zhaw.ch/remote.php/webdav/test/ # cloud output path to copy logs (only for k8s)
-  id: yaml-test # k8s job id (only for k8s)
-
 ```
 
-More sample tests can be found [here](samples/tests/)
+### Command-Line Interface
 
-You can use `python3 aerialist exec --help` anywhere to get help on the command parameters.
-Alternatively, the above test properties can also be set using commandline aruguments as below.
+#### Test Execution
 
-|argument   | input type            | description                   |
-|-----------|-----------------------|------------------------------ |
-| --test    | path/to/file.yml      | test description yaml file    |
-| --drone   | {**sitl**, ros, cf}   | type of the drone to conect to|
-| --mission | path/to/file.plan     | input mission file address    |
-| --params  | path/to/file.csv      | params file address           |
-| --simulator| {**gazebo**,jmavsim,ros} | the simulator environment to run|
-| --obstacle| float [l,w,h,x,y,z,r] | obstacle size, position, and angle to put in simulation environment|
-| --obstacle2| float [l,w,h,x,y,z,r]| obstacle size, position, and angle to put in simulation environment|  
-| --headless| _                     | whether to run the simulator headless|
-| --speed   | float = 1.0           | the simulator speed relative to real time|
-| --home    | float [lat,lon,alt]   | home position to place the drone|
-| --commands| path/to/file.{ulg,csv}| runtime commands file address |
-| --log     | path/to/file.ulg      | reference log file address    |
-| --agent   |{local,docker,k8s}     | where to run the tests        |
-| -n        | int = 1               | no. of parallel runs          |
-| --path    | path/to/folder/       | cloud output path to copy logs|
-| --id      | string                | k8s job id                    |
+You can utilize the toolkit with the following command line options:
 
-Some of the common combination of the following arguments are listed here as sample test cases:
-
-- Replaying a pre-recorded manual flight log:
-
-`python3 aerialist exec --commands samples/flights/manual1.ulg --simulator gazebo --drone sitl`
-
-- Running an existing series of manual commands stored in a csv file. Look [here](samples/flights/manual1-commands.csv) for an example (corresponding to previous .ulg file).
-
-`python3 aerialist exec --commands samples/flights/manual1-commands.csv --simulator gazebo --drone sitl`
+1. Make sure you are at the root directory of the repository:
+`cd Aerialist/`
 
-- Replaying a pre-recorded manual flight log with collission prevention enabled:
+2. **IMPORTANT NOTE**: The following commands are assuming that you are using Aerialist directly on your machine (not inside a docker container).
 
-`python3 aerialist exec --commands samples/flights/manual2.ulg --simulator ros --drone ros`
+- `--agent docker` is asking Aerialist to create a docker container and execute test cases there as detailed [above](#using-hosts-cli).
+- If you are executing the command [inside a docker container](#using-docker-containers-cli), exclude `--agent docker` to let the test run locally there.
 
-- Executing a pre-planed autonomous flight log with obstacle avoidance enabled:
+3. The simplest way to execute a UAV test by Aerialist is by the following command:
 
-`python3 aerialist exec --commands samples/flights/mission1-commands.csv --mission samples/flights/mission1.plan --params samples/flights/mission1-params.csv --log samples/flights/mission1.ulg --simulator ros --drone ros`
+    `python3 aerialsit exec --test [test-file.yaml] --agent docker`
 
-<!-- - running a manual flight through keyboard commands:
+- Replaying a pre-recorded manual flight (RC commands are extracted and replayed from a .ulg log)
 
-`python3 run.py experiment manual`
+  `python3 aerialist exec --test samples/tests/manual1.yaml --agent docker`
 
-Look [here](https://github.com/skhatiri/drone-experiments/blob/5b7950dc99318d08dacab61ea8686c6d65402438/px4/drone.py#L76) for possible commands to send -->
+- Executing an autonomous mission with a box-shaped obstacle in the environment
 
-## Software Architecture
+  `python3 aerialist exec --test samples/tests/mission1.yaml --agent docker`
 
-To evaluate a test definition, we generate and execute the corresponding simulated test case automatically. The test case automates all necessary steps: setting up the test environment, building/running the firmware code, running/configuring the simulator with the simulated world properties, connecting the simulated UAV to the firmware, and applying the UAV configurations from the test case properties at startup.
-Then, the test case commands are scheduled and sent to the UAV, the flight is monitored for any issues, and after test completion, the flight log file is extracted.
+- More sample tests can be found [here](samples/tests/)
 
-### Test Description
+4. When test exection is finished, you will have test results in `results/` folder:
 
-The de-facto testing standard of UAVs relies on manually-written system-level tests to test UAVs in the field. These tests are defined as specific software configurations (using parameters, config files, etc.), in a specific environment setup (e.g., obstacles placement, lightning conditions), and a set of runtime commands. Such runtime commands received during the UAV flight (from RC, GCS, onboard computers, etc.), make the UAV fly with a specific human observable behavior (e.g., flight trajectory, speed, distance to obstacles). We model a UAV test case as a set of test properties (e.g., ⟨configuration, environment, commands⟩) that control the flight and a set of pre-defined UAV expected states (e.g., ⟨trajectory positions⟩) during the flight. More specifically we define a test case by the following properties, which are fed to Aerialist as an input file, or command arguments, and refer to them as test description:
+- Flight Log of the executed test (.ulg)
+- Plot of the flight trajectory, as seen below
 
-- Configs: Drone configuration at startup (all parameter values and configuration files required to start the simulation).
-- Commands: Timestamped external commands from the ground station or the remote controller to the drone during the flight (e.g., change flight mode, go in a specific direction, enter mission mode).
-- Environment (optional): Simulated world’s configurations (e.g., used simulator, obstacles’ position and shape, wind speed and direction).
-- Expectation (optional): time series of certain sensor reading that the test flights are expected to follow closely.
+You can use `python3 aerialist exec --help` anywhere to get help on [other possible arguments](./docs/CLI.md).
 
-### Generator
+#### Plotting Executed Test Cases
 
-The Generator module deals with setting up the simulated world before testing UAVs in SIL mode.
-It sets up and prepares the simulation environment as described in the test description, in a specific simulator (e.g., Gazebo, jMAVSim), along with the described static and dynamic objects and simulated UAV.
-Configurator. module is responsible for setting up and initialising the UAV under test (either simulated or real) before flying the UAV, according to the  instructions in the test description. This includes building the code, connecting to the drone via MAVLink, setting the parameters, uploading any needed resources, etc.
+You can plot flight trajectory of the executed test cases using the following comnmand. plots are stored in `results/` folder.
 
-### Commander
+  `python3 aerialist plot --test [test-file.yaml] --log [test-log.ulg]`
 
-The Commander module is responsible for all the runtime communications to the UAV, including scheduling and sending the Remote Control (RC) commands (e.g., manual sticks, flight mode changes, arm/disarm), communications from Ground Controll Station (GCS) or the offboard commands coming from a companion computer.
+<p align="center"><img src="docs/test_plot.png" alt="sample test plot" width="60%"/></p>
 
-### Monitor
+### Using Aerialist in Your Code
 
-The Monitor module is the module responsible for runtime analysis of UAV state during the flight. Using MAVLink, we are able to subscribe to any messages communicated between PX4 modules, including sensor values. These messages allow monitoring any runtime checks described in the test description to evaluate monitoring functionalities before deploying on the UAV. The tested and finalised monitoring solutions can then be developed
-directly in the PX4 firmware as an on board module, or as a ROS module running on the companion computer.
+You can integrate Aerialist's Python package in your own code and directly define and execute UAV test cases with it.
+This can be specifically useful when you are working on test generation approaches for UAVs. An example of such usage of Aerialist can be found in [Surrealist](https://github.com/skhatiri/Surrealist).
 
-### Analyst
+1. `pip3 install git+https://github.com/skhatiri/Aerialist.git`
+2. Create an instance of [DroneTest](./aerialist/px4/drone_test.py) class and define your test case
+3. configure and execute the test case using your preferred test execution agent ([LocalAgent](./aerialist/px4/local_agent.py), [DockerAgent](./aerialist/px4/docker_agent.py), [K8sAgent](./aerialist/px4/k8s_agent.py))
 
-The Analyst module is responsible for any post-flight analysis, mostly based on the extracted flight log. It parses the ULog
-files, and extracts any important and relevant data to analyse test result based on the given expectations in the
-test description.
+- Take a look at our [code snippets](./samples/snippets/) for more details and sample codes.
 
 ## References
 
@@ -224,5 +294,12 @@ The software we developed is distributed under MIT license. See the [license](./
 
 ## Contacts
 
-- Sajad Khatiri
-  - Zurich University of Applied Science (ZHAW), Switzerland - <mazr@zhaw.ch>
+Feel free to use the [Discussions](https://github.com/skhatiri/Aerialist/discussions) section to ask your questions and look for answers.
+
+You can also contact us directly using email:
+
+- Sajad Khatiri (Zurich University of Applied Sciences) - <mazr@zhaw.ch>
+
+<!-- ## Contributing
+
+TODO -->
diff --git a/aerialist/entry.py b/aerialist/entry.py
index cc38a4e..972a82b 100755
--- a/aerialist/entry.py
+++ b/aerialist/entry.py
@@ -17,7 +17,6 @@
         SimulationConfig,
         TestConfig,
         AgentConfig,
-        Plot,
         DroneTestResult,
     )
 except:
@@ -31,7 +30,6 @@
         SimulationConfig,
         TestConfig,
         AgentConfig,
-        Plot,
         DroneTestResult,
     )
 
@@ -133,12 +131,12 @@ def arg_parse():
         "-n",
         default=1,
         type=int,
-        help="no. of parallel runs (in Docker)",
+        help="no. of parallel runs (in k8s)",
     )
     parser.add_argument(
         "--path",
         default=None,
-        help="cloud output path to copy logs",
+        help="cloud output path to copy logs (in k8s)",
     )
     parser.add_argument(
         "--id",
@@ -168,6 +166,14 @@ def arg_parse():
 def run_experiment(args):
     if args.test is not None:
         test = DroneTest.from_yaml(args.test)
+        if test.agent is None:
+            test.agent = AgentConfig(
+                engine=args.agent,
+                count=args.n,
+                path=args.path,
+                id=args.id,
+            )
+
     else:
         drone_config = DroneConfig(
             port=args.drone,
@@ -204,7 +210,7 @@ def run_experiment(args):
         )
     test_results = execute_test(test)
     logger.info(f"LOG:{test_results[0].log_file}")
-    Plot(test, test_results)
+    DroneTest.plot(test, test_results)
     # if args.cloud:
     #         exp.log = ulog_helper.upload(exp.log, args.output)
     #     print(f"LOG:{exp.log}")
@@ -220,7 +226,7 @@ def execute_test(test: DroneTest):
         agent = K8sAgent(test)
 
     logger.info("running the test...")
-    test_results = agent.run(test)
+    test_results = agent.run()
 
     logger.info("test finished...")
     return test_results
@@ -237,7 +243,7 @@ def plot_test(args):
         else:
             test_results = DroneTestResult.load_folder(args.log)
 
-    Plot(test, test_results)
+    DroneTest.plot(test, test_results)
 
 
 def config_loggers():
diff --git a/aerialist/px4/command.py b/aerialist/px4/command.py
index 05d9b4c..cfae92c 100644
--- a/aerialist/px4/command.py
+++ b/aerialist/px4/command.py
@@ -1,10 +1,8 @@
 from __future__ import annotations
 from statistics import mean
-import numpy as np
 import pandas as pd
 from enum import Enum
 from typing import List
-import ruptures as rpt
 from . import file_helper
 
 
@@ -41,7 +39,10 @@ def __init__(
         self.y = y
         self.z = z
         self.r = r
-        self.mode = mode
+        if type(mode) == FlightMode:
+            self.mode = mode
+        else:
+            self.mode = FlightMode(mode)
 
     def __str__(self) -> str:
         return f"{int(self.timestamp)}\t{self.mode.name}\t({self.x},{self.y},{self.z},{self.r})\n"
@@ -72,22 +73,6 @@ def project(self, x, y, z, r) -> Command:
         else:
             return self
 
-    @classmethod
-    def extract_segments(cls, commands: List[Command]) -> List[List[Command]]:
-        data = np.array([[c.x, c.y, c.z, c.r] for c in commands])
-        alg = rpt.Pelt(model="rbf").fit(data)
-        change_idx = alg.predict(pen=2)
-        auto_idx = [i for i, c in enumerate(commands) if c.mode != FlightMode.Setpoint]
-        auto_idx += [i + 1 for i in auto_idx if i < len(commands) - 1]
-        split_idx = list(set(change_idx + auto_idx + [0]))
-        split_idx.sort()
-        segments: List[Command] = []
-        for i in range(len(split_idx) - 1):
-            seg = commands[split_idx[i] : split_idx[i + 1]]
-            segments.append(seg)
-
-        return segments
-
     @classmethod
     def extract_params_from_csv(cls, address: str) -> dict:
         """extracts and returns parameters"""
@@ -142,7 +127,7 @@ def extract_from_csv(cls, address: str) -> List[Command]:
                     row.y,
                     row.z,
                     row.r,
-                    FlightMode(row.mode),
+                    row.mode,
                 )
             )
         return commands
diff --git a/aerialist/px4/docker_agent.py b/aerialist/px4/docker_agent.py
index 80bc257..1245d34 100644
--- a/aerialist/px4/docker_agent.py
+++ b/aerialist/px4/docker_agent.py
@@ -51,7 +51,7 @@ def format_command(
         )
         return cmd
 
-    def run(self, config: DroneTest):
+    def run(self):
         logger.debug(self.docker_cmd)
         replay_cmd = subprocess.run(self.docker_cmd, shell=True, capture_output=True)
         self.process_output(
@@ -161,39 +161,3 @@ def export_file(self, src, dest):
             f"docker cp {self.container_id}:'{src}' '{dest}'",
             shell=True,
         )
-
-    # @classmethod
-    # def replay_parallel(
-    #     cls,
-    #     runs: int = 1,
-    #     drone: str = config("DRONE", default="sim"),
-    #     env: str = config("SIMULATOR", default="gazebo"),
-    #     headless: bool = True,
-    #     speed: float = config("SPEED", default=1, cast=int),
-    #     log: str = None,
-    #     is_jmavsim=False,
-    #     params_csv=None,
-    #     obstacles: List[float] = None,
-    #     mission_file: str = None,
-    # ):
-    #     experiments: List[DockerExperiment] = []
-    #     for i in range(runs):
-    #         experiments.append(
-    #             DockerExperiment(
-    #                 drone,
-    #                 env,
-    #                 headless,
-    #                 speed,
-    #                 log,
-    #                 is_jmavsim,
-    #                 params_csv,
-    #                 obstacles,
-    #                 mission_file,
-    #             )
-    #         )
-
-    #     loop = asyncio.get_event_loop()
-    #     loop.run_until_complete(
-    #         asyncio.gather(*[e.replay_async() for e in experiments])
-    #     )
-    #     return [e for e in experiments if hasattr(e, "trajectory")]
diff --git a/aerialist/px4/drone.py b/aerialist/px4/drone.py
index 34b83ca..85b7766 100644
--- a/aerialist/px4/drone.py
+++ b/aerialist/px4/drone.py
@@ -2,8 +2,6 @@
 import time
 import sched
 from mavsdk import System, action, asyncio
-import keyboard
-from enum import Enum
 from decouple import config
 from .command import Command, FlightMode, DefaultCommands
 from .drone_test import DroneConfig, TestConfig
@@ -172,85 +170,3 @@ async def connect_async(self, mav_address):
                 # logger.info("-- Global position state is ok")
                 break
         logger.info("Conected to the drone")
-
-    ###################### manual control
-    def manual(self):
-        loop = asyncio.get_event_loop()
-        loop.run_until_complete(self.manual_async())
-
-    async def manual_async(self):
-        """must run the script with sudo"""
-        logger.info("in keyboard control")
-        logger.info(
-            "esc:\tend\nspace:\tarm\nenter:\tdisarm\n\nt:\ttakeoff\nl:\tland\np:\tposition\nm:\tmanual\n\nwasd & up down left right: direction"
-        )
-        in_setpoint_mode = False
-        while True:
-            try:
-                command = None
-                if keyboard.is_pressed("esc"):
-                    return
-                if keyboard.is_pressed("space"):
-                    await self.run_long_async(
-                        DefaultCommands.Hover, 3.1 * self.SETPOINT_PERIOD
-                    )
-                    await self.run_async(DefaultCommands.Arm)
-                    logger.info("Armed")
-                    await self.run_long_async(
-                        DefaultCommands.Hover, 3.1 * self.SETPOINT_PERIOD
-                    )
-                    await self.run_async(DefaultCommands.Position)
-                    logger.info("in position mode")
-                    in_setpoint_mode = True
-                    continue
-                if keyboard.is_pressed("enter"):
-                    await self.run_async(DefaultCommands.Disarm)
-                    in_setpoint_mode = False
-                    break
-
-                if keyboard.is_pressed("t"):
-                    await self.run_async(DefaultCommands.Takeoff)
-                    in_setpoint_mode = False
-                if keyboard.is_pressed("l"):
-                    await self.run_async(DefaultCommands.Land)
-                    in_setpoint_mode = False
-
-                if keyboard.is_pressed("p"):
-                    await self.run_async(DefaultCommands.Position)
-                    in_setpoint_mode = True
-
-                if keyboard.is_pressed("m"):
-                    await self.run_async(DefaultCommands.Manual)
-                    in_setpoint_mode = True
-
-                if keyboard.is_pressed("w"):
-                    command = DefaultCommands.Up
-                if keyboard.is_pressed("s"):
-                    command = DefaultCommands.Down
-                if keyboard.is_pressed("a"):
-                    command = DefaultCommands.Spin_left
-                if keyboard.is_pressed("d"):
-                    command = DefaultCommands.Spin_right
-                if keyboard.is_pressed("up"):
-                    command = DefaultCommands.Fornt
-                if keyboard.is_pressed("down"):
-                    command = DefaultCommands.Back
-                if keyboard.is_pressed("left"):
-                    command = DefaultCommands.Left
-                if keyboard.is_pressed("right"):
-                    command = DefaultCommands.Right
-
-                if command != None:
-                    in_setpoint_mode = True
-
-                if in_setpoint_mode and command == None:
-                    command = DefaultCommands.Hover
-
-                if command != None:
-                    await self.run_async(command)
-
-                await asyncio.sleep(self.SETPOINT_PERIOD)
-
-            except Exception as e:
-                logger.error("Unexpected error:" + str(e))
-                continue
diff --git a/aerialist/px4/file_helper.py b/aerialist/px4/file_helper.py
index 5f5dcfe..96fbb22 100644
--- a/aerialist/px4/file_helper.py
+++ b/aerialist/px4/file_helper.py
@@ -12,7 +12,6 @@
 import webdav3.exceptions
 from os import path
 import os
-import validators
 
 logger = logging.getLogger(__name__)
 MAX_WEBDAV_RETRIES = 5
@@ -20,7 +19,7 @@
 
 
 def get_local_file(file_path: str):
-    if validators.url(file_path):
+    if is_webdav_address(file_path):
         local_add = download(file_path, config("WEBDAV_DL_FLD", default="tmp/"))
         return local_add
     elif path.exists(file_path):
@@ -29,8 +28,16 @@ def get_local_file(file_path: str):
         raise Exception("path does not exist")
 
 
+def is_webdav_address(address: str):
+    return address.startswith("webdav://")
+
+
+def get_webdav_path(address: str):
+    return address.replace("webdav://", "")
+
+
 def get_local_folder(folder_path: str):
-    if validators.url(folder_path):
+    if is_webdav_address(folder_path):
         if folder_path.endswith("/"):
             folder = folder_path.split("/")[-2]
         else:
@@ -103,8 +110,8 @@ def upload(src_file: str, dest_path: str) -> str:
     global RETRIES
     dest_path += path.basename(src_file)
     cloud_path = dest_path
-    if validators.url(dest_path):
-        cloud_path = dest_path.replace(config("WEBDAV_ROOT"), "")
+    if is_webdav_address(dest_path):
+        cloud_path = get_webdav_path(dest_path)
     try:
         webdav_client.upload_file(cloud_path, src_file)
         RETRIES = 0
@@ -123,8 +130,8 @@ def upload(src_file: str, dest_path: str) -> str:
 def download(src_file: str, dest_path: str) -> str:
     global RETRIES
     cloud_path = src_file
-    if validators.url(src_file):
-        cloud_path = src_file.replace(config("WEBDAV_ROOT"), "")
+    if is_webdav_address(src_file):
+        cloud_path = get_webdav_path(src_file)
     dest_path += path.basename(cloud_path)
     try:
         webdav_client.download_file(cloud_path, dest_path)
@@ -144,8 +151,8 @@ def download(src_file: str, dest_path: str) -> str:
 def download_dir(src_path: str, dest_path: str) -> str:
     global RETRIES
     cloud_path = src_path
-    if validators.url(src_path):
-        cloud_path = src_path.replace(config("WEBDAV_ROOT"), "")
+    if is_webdav_address(src_path):
+        cloud_path = get_webdav_path(src_path)
     try:
         webdav_client.download_directory(cloud_path, dest_path)
         RETRIES = 0
@@ -164,8 +171,8 @@ def download_dir(src_path: str, dest_path: str) -> str:
 def create_dir(path: str):
     global RETRIES
     cloud_path = path
-    if validators.url(path):
-        cloud_path = path.replace(config("WEBDAV_ROOT"), "")
+    if is_webdav_address(path):
+        cloud_path = get_webdav_path(path)
     try:
         if not webdav_client.check(cloud_path):
             webdav_client.mkdir(cloud_path)
diff --git a/aerialist/px4/k8s_agent.py b/aerialist/px4/k8s_agent.py
index abd413d..57b8ab5 100644
--- a/aerialist/px4/k8s_agent.py
+++ b/aerialist/px4/k8s_agent.py
@@ -6,7 +6,6 @@
 import subprocess
 from typing import List
 from decouple import config
-
 from .command import Command
 from .docker_agent import DockerAgent
 from .drone_test import AgentConfig, DroneTest, DroneTestResult, SimulationConfig
@@ -16,33 +15,69 @@
 
 
 class K8sAgent(DockerAgent):
+    KUBE_CMD = 'yq \'.metadata.name = "{name}" | .spec.template.spec.containers[0].env |= map(select(.name == "COMMAND").value = "{command}") | .spec.completions = {runs} | .spec.parallelism = {runs}\' {template} | kubectl apply -f - --validate=true'
+    KUBE_LOCAL_CMD = 'yq \'.metadata.name = "{name}" | .spec.template.spec.volumes[0].hostPath.path = "{host_volume}" | .spec.template.spec.containers[0].env |= map(select(.name == "COMMAND").value = "{command}") | .spec.completions = {runs} | .spec.parallelism = {runs}\' {template} | kubectl apply -f - --validate=true'
 
-    KUBE_CMD = 'yq \'.metadata.name = "{name}" | .spec.template.spec.containers[0].env |= map(select(.name == "COMMAND").value="{command}") | .spec.completions={runs} | .spec.parallelism={runs}\' {template} | kubectl apply -f - --validate=false'
+    CMD = "timeout {timeout} python3 aerialist exec --test {test_file}"
     WEBDAV_LOCAL_DIR = config("WEBDAV_DL_FLD", default="tmp/")
     DEFAULT_KUBE_TEMPLATE = config(
         "KUBE_TEMPLATE", default="aerialist/resources/k8s/k8s-job.yaml"
     )
+    DEFAULT_LOCAL_KUBE_TEMPLATE = config(
+        "LOCAL_KUBE_TEMPLATE", default="aerialist/resources/k8s/k8s-job-local.yaml"
+    )
     ROS_KUBE_TEMPLATE = config(
         "ROS_KUBE_TEMPLATE", default="aerialist/resources/k8s/k8s-job-avoidance.yaml"
     )
+    ROS_LOCAL_KUBE_TEMPLATE = config(
+        "ROS_LOCAL_KUBE_TEMPLATE",
+        default="aerialist/resources/k8s/k8s-job-avoidance-local.yaml",
+    )
+    USE_VOLUME = config("KUBE_USE_VOLUME", cast=bool, default=False)
+    VOLUME_PATH = config("KUBE_VOLUME_PATH", default="/src/aerialist/results/")
 
     def __init__(self, config: DroneTest) -> None:
         self.config = config
         self.results: List[DroneTestResult] = []
         self.k8s_config = self.import_config()
 
-    def run(self, config: DroneTest):
-        cmd = self.format_command(self.k8s_config)
-
-        logger.debug("docker command:" + cmd)
-        kube_cmd = self.KUBE_CMD.format(
-            name=self.config.agent.id,
-            command=cmd,
-            runs=self.config.agent.count,
-            template=self.ROS_KUBE_TEMPLATE
-            if self.config.simulation.simulator == SimulationConfig.ROS
-            else self.DEFAULT_KUBE_TEMPLATE,
+    def run(self):
+        cmd = self.CMD.format(
+            test_file=self.k8s_test_yaml,
+            timeout=self.DOCKER_TIMEOUT,
         )
+        logger.debug("docker command:" + cmd)
+
+        host_volume_prefix = ""
+
+        if self.USE_VOLUME:
+            if self.config.simulation.simulator == SimulationConfig.ROS:
+                template = self.ROS_LOCAL_KUBE_TEMPLATE
+            else:
+                template = self.DEFAULT_LOCAL_KUBE_TEMPLATE
+
+            host_volume_prefix = "/host_mnt"
+            kube_cmd = self.KUBE_LOCAL_CMD.format(
+                name=self.config.agent.id,
+                command=cmd,
+                host_volume=host_volume_prefix + self.config.agent.path,
+                runs=self.config.agent.count,
+                template=template,
+            )
+
+        else:
+            if self.config.simulation.simulator == SimulationConfig.ROS:
+                template = self.ROS_KUBE_TEMPLATE
+            else:
+                template = self.DEFAULT_KUBE_TEMPLATE
+
+            kube_cmd = self.KUBE_CMD.format(
+                name=self.config.agent.id,
+                command=cmd,
+                runs=self.config.agent.count,
+                template=template,
+            )
+
         logger.debug("k8s command:" + kube_cmd)
         logger.info("creating k8s job")
         kube_prc = subprocess.run(kube_cmd, shell=True)
@@ -51,11 +86,14 @@ def run(self, config: DroneTest):
             loop = asyncio.get_event_loop()
             succes = loop.run_until_complete(self.wait_success(self.config.agent.id))
             logger.info("k8s job finished")
-            local_folder = f"{self.WEBDAV_LOCAL_DIR}{self.config.agent.id}/"
-            os.makedirs(local_folder, exist_ok=True)
-            logger.info(f"downloading simulation logs to {local_folder}")
-            file_helper.download_dir(self.k8s_config.agent.path, local_folder)
-            logger.debug("files downloaded")
+            if self.USE_VOLUME:
+                local_folder = self.config.agent.path
+            else:
+                local_folder = f"{self.WEBDAV_LOCAL_DIR}{self.config.agent.id}/"
+                os.makedirs(local_folder, exist_ok=True)
+                logger.info(f"downloading simulation logs to {local_folder}")
+                file_helper.download_dir(self.k8s_config.agent.path, local_folder)
+                logger.debug("files downloaded")
 
             for test_log in os.listdir(local_folder):
                 if test_log.endswith(".ulg") and (
@@ -89,6 +127,14 @@ def import_config(self):
         if not self.config.agent.path.endswith("/"):
             self.config.agent.path += "/"
         self.config.agent.path += self.config.agent.id + "/"
+
+        if self.USE_VOLUME:
+            k8s_config = self.copy_files_volume()
+        else:
+            k8s_config = self.copy_files_cloud()
+        return k8s_config
+
+    def copy_files_cloud(self):
         cloud_folder = self.config.agent.path
 
         k8s_config = deepcopy(self.config)
@@ -112,7 +158,7 @@ def import_config(self):
                 and self.config.test.commands_file is None
             ):
                 self.config.test.commands_file = (
-                    f"/tmp/{file_helper.time_filename()}.csv"
+                    f"{self.WEBDAV_LOCAL_DIR}{file_helper.time_filename()}.csv"
                 )
                 Command.save_csv(
                     self.config.test.commands, self.config.test.commands_file
@@ -134,9 +180,66 @@ def import_config(self):
             k8s_config.agent.engine = AgentConfig.LOCAL
             k8s_config.agent.count = 1
 
+        self.k8s_test_yaml = k8s_config.to_yaml(
+            f"{self.WEBDAV_LOCAL_DIR}{self.config.agent.id}.yaml"
+        )
+        self.k8s_test_yaml = file_helper.upload(self.k8s_test_yaml, cloud_folder)
         logger.info(f"files uploaded")
         return k8s_config
 
+    def copy_files_volume(self):
+        volume_folder = self.config.agent.path
+
+        k8s_config = deepcopy(self.config)
+        os.makedirs(volume_folder, exist_ok=True)
+
+        # Drone Config
+        if self.config.drone is not None:
+            if self.config.drone.mission_file is not None:
+                k8s_config.drone.mission_file = file_helper.copy(
+                    self.config.drone.mission_file, volume_folder
+                ).replace(volume_folder, self.VOLUME_PATH)
+            if self.config.drone.params_file is not None:
+                k8s_config.drone.params_file = file_helper.copy(
+                    self.config.drone.params_file, volume_folder
+                ).replace(volume_folder, self.VOLUME_PATH)
+
+        # Test Config
+        if self.config.test is not None:
+            if (
+                self.config.test.commands is not None
+                and self.config.test.commands_file is None
+            ):
+                self.config.test.commands_file = (
+                    f"/tmp/{file_helper.time_filename()}.csv"
+                )
+                Command.save_csv(
+                    self.config.test.commands, self.config.test.commands_file
+                )
+            if self.config.test.commands_file is not None:
+                k8s_config.test.commands_file = file_helper.copy(
+                    self.config.test.commands_file, volume_folder
+                ).replace(volume_folder, self.VOLUME_PATH)
+
+        # Assertion Config
+        k8s_config.assertion = None
+        # if self.config.assertion is not None:
+        #     if self.config.assertion.log_file is not None:
+        #         k8s_config.assertion.log_file = file_helper.upload(
+        #             self.config.assertion.log_file, cloud_folder
+        #         )
+
+        if k8s_config.agent is not None:
+            k8s_config.agent.engine = AgentConfig.LOCAL
+            k8s_config.agent.count = 1
+            k8s_config.agent.path = None
+
+        self.k8s_test_yaml = k8s_config.to_yaml(
+            f"{volume_folder}{self.config.agent.id}.yaml"
+        ).replace(volume_folder, self.VOLUME_PATH)
+        logger.info(f"files copied")
+        return k8s_config
+
     async def wait_success(self, job_id):
         completed = await asyncio.create_subprocess_shell(
             f"kubectl wait --for=condition=complete  --timeout=1000s job.batch/{job_id}"
diff --git a/aerialist/px4/local_agent.py b/aerialist/px4/local_agent.py
index 7569b0a..6d2fc26 100644
--- a/aerialist/px4/local_agent.py
+++ b/aerialist/px4/local_agent.py
@@ -3,6 +3,7 @@
 from .drone import Drone
 from .simulator import Simulator
 from .drone_test import AssertionConfig, DroneTest, DroneTestResult
+from . import tools
 
 
 class LocalAgent(TestAgent):
@@ -18,7 +19,7 @@ def __init__(self, config: DroneTest) -> None:
             if self.config.test is not None:
                 self.drone.schedule_test(self.config.test)
 
-    def run(self, config: DroneTest):
+    def run(self):
         try:
             self.drone.run_scheduled()
             log = self.simulator.get_log()
@@ -31,5 +32,5 @@ def run(self, config: DroneTest):
             raise (e)
 
     def manual(self):
-        self.drone.manual()
+        tools.manual_flight(self.drone)
         self.log = Simulator.get_log()
diff --git a/aerialist/px4/obstacle.py b/aerialist/px4/obstacle.py
index f4cecc8..99be1d8 100644
--- a/aerialist/px4/obstacle.py
+++ b/aerialist/px4/obstacle.py
@@ -1,56 +1,64 @@
 from __future__ import annotations
-from typing import List
+from typing import List, NamedTuple
 from shapely.geometry import box, LineString, Point
 from shapely import affinity
 import matplotlib.patches as mpatches
 from decouple import config
 import logging
-from .position import Position
 
 logger = logging.getLogger(__name__)
 
 
 class Obstacle(object):
-    DIR = config("RESULTS_DIR", default="results/")
+    class Size(NamedTuple):
+        l: float
+        w: float
+        h: float
+
+    class Position(NamedTuple):
+        x: float
+        y: float
+        z: float = 0
+        r: float = 0
+
     BOX = "box"
     CENTER_POSITION = True
 
     def __init__(
         self,
-        size: Position,
+        size: Size,
         position: Position,
-        angle: float = 0,
         shape: str = BOX,
     ) -> None:
         super().__init__()
+
         if shape == self.BOX:
             if self.CENTER_POSITION:
-                rect = box(-size.x / 2, -size.y / 2, size.x / 2, size.y / 2)
+                rect = box(-size.l / 2, -size.w / 2, size.l / 2, size.w / 2)
             else:
-                rect = box(0, 0, size.x, size.y)
-            self.geometry = affinity.rotate(rect, angle, "centroid")
+                rect = box(0, 0, size.l, size.w)
+            self.geometry = affinity.rotate(rect, position.r, "centroid")
             self.geometry = affinity.translate(self.geometry, position.x, position.y)
             self.unrotated_geometry = affinity.translate(rect, position.x, position.y)
             self.size = size
             self.position = position
-            self.angle = angle
 
     def center(self):
-        return Position(
+        return Obstacle.Position(
             self.geometry.centroid.coords[0][0],
             self.geometry.centroid.coords[0][1],
-            self.size.z / 2,
+            self.size.h / 2,
         )
 
     def anchor_point(self):
-        return Position(
+        return Obstacle.Position(
             self.unrotated_geometry.bounds[0],
             self.unrotated_geometry.bounds[1],
             0,
         )
 
     def corner(self):
-        return Position(
+        return Obstacle.Position(
             self.geometry.bounds[0],
             self.geometry.bounds[1],
             0,
@@ -58,13 +66,13 @@ def corner(self):
 
     def to_params(self):
         return [
-            self.size.x,
-            self.size.y,
-            self.size.z,
+            self.size.l,
+            self.size.w,
+            self.size.h,
             self.position.x,
             self.position.y,
             self.position.z,
-            self.angle,
+            self.position.r,
         ]
 
     def plt_patch(self):
@@ -73,9 +81,9 @@ def plt_patch(self):
                 self.anchor_point().x,
                 self.anchor_point().y,
             ),
-            self.size.x,
-            self.size.y,
-            self.angle,
+            self.size.l,
+            self.size.w,
+            self.position.r,
             rotation_point="center",
             edgecolor="gray",
             facecolor="gray",
@@ -90,22 +98,30 @@ def intersects(self, other: Obstacle):
     def distance(self, geometry):
         return self.geometry.distance(geometry)
 
+    def to_dict(self):
+        return {
+            "size": {"l": self.size.l, "w": self.size.w, "h": self.size.h},
+            "position": {
+                "x": self.position.x,
+                "y": self.position.y,
+                "z": self.position.z,
+                "r": self.position.r,
+            },
+        }
+
     @classmethod
     def distance_to_many(cls, obstacles: List[Obstacle], line: LineString):
         boxes = [o.geometry for o in obstacles]
-        # for i in range(len(boxes)):
-        #     for j in range(i + 1, len(boxes)):
-        #         if boxes[i].intersects(boxes[j]):
-        #             return 1000
         dist = min([sum([b.distance(Point(*p)) for b in boxes]) for p in line.coords])
         return dist
 
     @classmethod
     def from_coordinates(cls, coordinates: List[float]):
-        size = Position(coordinates[0], coordinates[1], coordinates[2])
-        position = Position(coordinates[3], coordinates[4], coordinates[5])
-        angle = coordinates[6]
-        return Obstacle(size, position, angle)
+        size = Obstacle.Size(coordinates[0], coordinates[1], coordinates[2])
+        position = Obstacle.Position(
+            coordinates[3], coordinates[4], coordinates[5], coordinates[6]
+        )
+        return Obstacle(size, position)
 
     @classmethod
     def from_coordinates_multiple(cls, coordinates: List[float]):
@@ -114,82 +130,17 @@ def from_coordinates_multiple(cls, coordinates: List[float]):
             obst.append(Obstacle.from_coordinates(coordinates[i : i + 7]))
         return obst
 
-    # def estimate_box_pose(
-    #     self, positions: List[Position], size: Position = None
-    # ) -> Tuple[Position, Position]:
-    #     min_pos = Position(
-    #         min(p.x for p in positions),
-    #         min(p.y for p in positions),
-    #         min(abs(p.z) for p in positions),
-    #     )
-    #     max_pos = Position(
-    #         max(p.x for p in positions),
-    #         max(p.y for p in positions),
-    #         max(abs(p.z) for p in positions),
-    #     )
-    #     if size == None:
-    #         size = (max_pos.x - min_pos.x, max_pos.y - min_pos.y, max_pos.z)
-
-    #     location = Position(
-    #         (max_pos.x + min_pos.x) / 2,
-    #         (max_pos.y + min_pos.y) / 2,
-    #         size[2] / 2,
-    #     )
-
-    #     logger.info(location, size)
-    #     return location, size
-
-    # @classmethod
-    # def extract_from_log(cls, log_address: str, size: Position = None):
-    #     """extracts and returns detected obstacle borders from the input log"""
-
-    #     avoidance_active_periods = Trajectory.extract_CP_active_periods(log_address)
-
-    #     points: List[Position] = []
-    #     idx = 0
-
-    #     obstacle_distance = ulog_helper.extract(log_address, "obstacle_distance_fused")
-    #     # dropping duplicate rows (some rows are repated in the logs with the same timestamps)
-    #     obstacle_distance.drop_duplicates("timestamp", inplace=True)
-    #     for i, row in obstacle_distance.iterrows():
-    #         tmstmp = int(row["timestamp"])
-    #         if tmstmp < avoidance_active_periods[idx][0]:
-    #             continue
-    #         elif tmstmp <= avoidance_active_periods[idx][1]:
-    #             p = Position(
-    #                 None,
-    #                 None,
-    #                 None,
-    #                 timestamp=row.timestamp,
-    #             )
-    #             p.distance = (
-    #                 float(row["distances[0]"]) / 100
-    #             )  # convert to meters (originally cm)
-    #             points.append(p)
-
-    #         else:
-    #             idx += 1
-    #             if idx >= len(avoidance_active_periods):
-    #                 break
-    #         # logger.info (points)
-
-    #     trajectory = Trajectory.extract_from_log(log_address)
-    #     trj_times = [p.timestamp for p in trajectory.positions]
-    #     for p in points:
-    #         # find nearest data in local positions
-    #         idx = timeserie_helper.find_nearest_index(trj_times, p.timestamp)
-
-    #         # updating position based on yaw angle
-    #         obstacle_point = trajectory.positions[
-    #             idx
-    #         ].get_position_in_relative_distance(p.distance)
-
-    #         p.x, p.y, p.z, p.r = (
-    #             obstacle_point.x,
-    #             obstacle_point.y,
-    #             obstacle_point.z,
-    #             obstacle_point.r,
-    #         )
-
-    #     logger.info(points)
-    #     return cls(points, size=size)
+    @classmethod
+    def from_dict(cls, obstacle: dict):
+        size = Obstacle.Size(obstacle.size.l, obstacle.size.w, obstacle.size.h)
+        position = Obstacle.Position(
+            obstacle.position.x,
+            obstacle.position.y,
+            obstacle.position.z,
+            obstacle.position.r,
+        )
+        return Obstacle(size, position)
+
+    @classmethod
+    def from_dict_multiple(cls, obstacles: List[dict]):
+        return [cls.from_dict(obs) for obs in obstacles]
diff --git a/aerialist/px4/position.py b/aerialist/px4/position.py
index 2e773ec..1c6a10d 100644
--- a/aerialist/px4/position.py
+++ b/aerialist/px4/position.py
@@ -2,6 +2,7 @@
 import math
 from statistics import mean
 from typing import List
+from shapely.geometry import Point
 
 
 class Position(object):
@@ -12,7 +13,6 @@ def __init__(
         z: float,
         r: float = None,
         timestamp: int = None,
-        is_jmavsim: bool = False,
     ) -> None:
         super().__init__()
         self.timestamp = timestamp
@@ -21,9 +21,6 @@ def __init__(
         self.z = z
         self.r = r
 
-        if is_jmavsim:
-            self.convert_jmavsim()
-
     def convert_jmavsim(self):
         jmav_x = self.x
         jmav_y = self.y
@@ -35,6 +32,9 @@ def get_position_in_relative_distance(self, distance: float):
         delta_y = distance * math.sin(self.r)
         return Position(self.x + delta_x, self.y + delta_y, self.z, self.r)
 
+    def to_geometry(self):
+        return Point(self.x, self.y)
+
     def to_dict(self):
         return {
             "timestamp": self.timestamp,
@@ -56,5 +56,6 @@ def average(cls, positions: List[Position]):
             x=mean([p.x for p in positions]),
             y=mean([p.y for p in positions]),
             z=mean([p.z for p in positions]),
+            r=mean([p.r for p in positions]),
             timestamp=mean([p.timestamp for p in positions]),
         )
diff --git a/aerialist/px4/simulator.py b/aerialist/px4/simulator.py
index e9653a9..d2e85b8 100644
--- a/aerialist/px4/simulator.py
+++ b/aerialist/px4/simulator.py
@@ -61,10 +61,15 @@ def __init__(self, config: SimulationConfig) -> None:
                 + f'{self.CATKIN_DIR}src/avoidance/avoidance/sim/models:{self.CATKIN_DIR}src/avoidance/avoidance/sim/worlds" >> ~/.bashrc; '
             )
             sim_command += f"exec roslaunch {self.AVOIDANCE_LAUNCH} gui:={str((not self.config.headless) and self.GAZEBO_GUI_AVOIDANCE).lower()} rviz:={str(False and not self.config.headless).lower()} world_file_name:={self.AVOIDANCE_WORLD} "
-            if self.config.obstacles != None and len(self.config.obstacles) > 0:
-                sim_command += f"obst:=true obst_x:={self.config.obstacles[0].position.y} obst_y:={self.config.obstacles[0].position.x} obst_z:={self.config.obstacles[0].position.z} obst_l:={self.config.obstacles[0].size.y} obst_w:={self.config.obstacles[0].size.x} obst_h:={self.config.obstacles[0].size.z} obst_yaw:={math.radians(-self.config.obstacles[0].angle)} "
+            if self.config.obstacles != None:
+                if len(self.config.obstacles) > 0:
+                    sim_command += f"obst:=true obst_x:={self.config.obstacles[0].position.y} obst_y:={self.config.obstacles[0].position.x} obst_z:={self.config.obstacles[0].position.z} obst_l:={self.config.obstacles[0].size.w} obst_w:={self.config.obstacles[0].size.l} obst_h:={self.config.obstacles[0].size.h} obst_yaw:={math.radians(-self.config.obstacles[0].position.r)} "
                 if len(self.config.obstacles) > 1:
-                    sim_command += f"obst2:=true obst2_x:={self.config.obstacles[1].position.y} obst2_y:={self.config.obstacles[1].position.x} obst2_z:={self.config.obstacles[1].position.z} obst2_l:={self.config.obstacles[1].size.y} obst2_w:={self.config.obstacles[1].size.x} obst2_h:={self.config.obstacles[1].size.z} obst2_yaw:={math.radians(-self.config.obstacles[1].angle)} "
+                    sim_command += f"obst2:=true obst2_x:={self.config.obstacles[1].position.y} obst2_y:={self.config.obstacles[1].position.x} obst2_z:={self.config.obstacles[1].position.z} obst2_l:={self.config.obstacles[1].size.w} obst2_w:={self.config.obstacles[1].size.l} obst2_h:={self.config.obstacles[1].size.h} obst2_yaw:={math.radians(-self.config.obstacles[1].position.r)} "
+                if len(self.config.obstacles) > 2:
+                    sim_command += f"obst3:=true obst3_x:={self.config.obstacles[2].position.y} obst3_y:={self.config.obstacles[2].position.x} obst3_z:={self.config.obstacles[2].position.z} obst3_l:={self.config.obstacles[2].size.w} obst3_w:={self.config.obstacles[2].size.l} obst3_h:={self.config.obstacles[2].size.h} obst3_yaw:={math.radians(-self.config.obstacles[2].position.r)} "
+                if len(self.config.obstacles) > 3:
+                    sim_command += f"obst4:=true obst4_x:={self.config.obstacles[3].position.y} obst4_y:={self.config.obstacles[3].position.x} obst4_z:={self.config.obstacles[3].position.z} obst4_l:={self.config.obstacles[3].size.w} obst4_w:={self.config.obstacles[3].size.l} obst4_h:={self.config.obstacles[3].size.h} obst4_yaw:={math.radians(-self.config.obstacles[3].position.r)} "
 
         logger.debug("executing:" + sim_command)
         self.sim_process = subprocess.Popen(
diff --git a/aerialist/px4/timeserie_helper.py b/aerialist/px4/timeserie_helper.py
index 013e780..3b8df5d 100644
--- a/aerialist/px4/timeserie_helper.py
+++ b/aerialist/px4/timeserie_helper.py
@@ -31,25 +31,3 @@ def normalize(data: np.ndarray):
         d_std = np.std(data[:, d])
         normal[:, d] = (data[:, d] - d_mean) / d_std
     return normal
-
-
-def dtw_tweaked(exp_data, num_data, metric="euclidean", **kwargs):
-    if len(exp_data) > len(num_data):  # ensure first array is shorter
-        tmp = exp_data
-        exp_data = num_data
-        num_data = tmp
-    c = distance.cdist(exp_data, num_data, metric=metric, **kwargs)
-    d = np.zeros(c.shape)
-    d[0, 0] = c[0, 0]
-    n, m = c.shape
-    for i in range(1, n):
-        d[i, 0] = c.max()  # d[i - 1, 0]
-    for j in range(1, m):
-        d[0, j] = d[0, j - 1]
-    for i in range(1, n):
-        for j in range(1, m):
-            # d[i, j] = c[i, j] + min((d[i - 1, j], d[i, j - 1], d[i - 1, j - 1]))
-            d[i, j] = min(
-                (d[i, j - 1], c[i, j] + d[i - 1, j - 1])
-            )  # only matching points have a cost; insertions forbidden; deletions free
-    return d[-1, -1], d
diff --git a/aerialist/px4/tools.py b/aerialist/px4/tools.py
new file mode 100644
index 0000000..d0ff653
--- /dev/null
+++ b/aerialist/px4/tools.py
@@ -0,0 +1,228 @@
+from typing import List, Tuple
+import numpy as np
+import ruptures as rpt
+import logging
+from mavsdk import asyncio
+import keyboard
+
+from . import timeserie_helper, file_helper
+from .command import Command, FlightMode, DefaultCommands
+from .drone import Drone
+from .position import Position
+from .trajectory import Trajectory
+
+logger = logging.getLogger(__name__)
+
+
+### Command:
+def extract_command_segments(commands: List[Command]) -> List[List[Command]]:
+    """extract segments of manual commadns"""
+    data = np.array([[c.x, c.y, c.z, c.r] for c in commands])
+    alg = rpt.Pelt(model="rbf").fit(data)
+    change_idx = alg.predict(pen=2)
+    auto_idx = [i for i, c in enumerate(commands) if c.mode != FlightMode.Setpoint]
+    auto_idx += [i + 1 for i in auto_idx if i < len(commands) - 1]
+    split_idx = list(set(change_idx + auto_idx + [0]))
+    split_idx.sort()
+    segments: List[Command] = []
+    for i in range(len(split_idx) - 1):
+        seg = commands[split_idx[i] : split_idx[i + 1]]
+        segments.append(seg)
+
+    return segments
+
+
+#### Drone:
+def manual_flight(drone: Drone):
+    """manually send RC commands using the keyoard keys to the drone"""
+    loop = asyncio.get_event_loop()
+    loop.run_until_complete(manual_flight_async(drone))
+
+
+#### Drone:
+async def manual_flight_async(drone: Drone):
+    """must run the script with sudo"""
+    logger.info("in keyboard control")
+    logger.info(
+        "esc:\tend\nspace:\tarm\nenter:\tdisarm\n\nt:\ttakeoff\nl:\tland\np:\tposition\nm:\tmanual\n\nwasd & up down left right: direction"
+    )
+    in_setpoint_mode = False
+    while True:
+        try:
+            command = None
+            if keyboard.is_pressed("esc"):
+                return
+            if keyboard.is_pressed("space"):
+                await drone.run_long_async(
+                    DefaultCommands.Hover, 3.1 * drone.SETPOINT_PERIOD
+                )
+                await drone.run_async(DefaultCommands.Arm)
+                logger.info("Armed")
+                await drone.run_long_async(
+                    DefaultCommands.Hover, 3.1 * drone.SETPOINT_PERIOD
+                )
+                await drone.run_async(DefaultCommands.Position)
+                logger.info("in position mode")
+                in_setpoint_mode = True
+                continue
+            if keyboard.is_pressed("enter"):
+                await drone.run_async(DefaultCommands.Disarm)
+                in_setpoint_mode = False
+                break
+
+            if keyboard.is_pressed("t"):
+                await drone.run_async(DefaultCommands.Takeoff)
+                in_setpoint_mode = False
+            if keyboard.is_pressed("l"):
+                await drone.run_async(DefaultCommands.Land)
+                in_setpoint_mode = False
+
+            if keyboard.is_pressed("p"):
+                await drone.run_async(DefaultCommands.Position)
+                in_setpoint_mode = True
+
+            if keyboard.is_pressed("m"):
+                await drone.run_async(DefaultCommands.Manual)
+                in_setpoint_mode = True
+
+            if keyboard.is_pressed("w"):
+                command = DefaultCommands.Up
+            if keyboard.is_pressed("s"):
+                command = DefaultCommands.Down
+            if keyboard.is_pressed("a"):
+                command = DefaultCommands.Spin_left
+            if keyboard.is_pressed("d"):
+                command = DefaultCommands.Spin_right
+            if keyboard.is_pressed("up"):
+                command = DefaultCommands.Fornt
+            if keyboard.is_pressed("down"):
+                command = DefaultCommands.Back
+            if keyboard.is_pressed("left"):
+                command = DefaultCommands.Left
+            if keyboard.is_pressed("right"):
+                command = DefaultCommands.Right
+
+            if command != None:
+                in_setpoint_mode = True
+
+            if in_setpoint_mode and command == None:
+                command = DefaultCommands.Hover
+
+            if command != None:
+                await drone.run_async(command)
+
+            await asyncio.sleep(drone.SETPOINT_PERIOD)
+
+        except Exception as e:
+            logger.error("Unexpected error:" + str(e))
+            continue
+
+
+### Obstacle:
+def estimate_box_pose(
+    positions: List[Position], size: Position = None
+) -> Tuple[Position, Position]:
+    min_pos = Position(
+        min(p.x for p in positions),
+        min(p.y for p in positions),
+        min(abs(p.z) for p in positions),
+    )
+    max_pos = Position(
+        max(p.x for p in positions),
+        max(p.y for p in positions),
+        max(abs(p.z) for p in positions),
+    )
+    if size == None:
+        size = (max_pos.x - min_pos.x, max_pos.y - min_pos.y, max_pos.z)
+
+    location = Position(
+        (max_pos.x + min_pos.x) / 2,
+        (max_pos.y + min_pos.y) / 2,
+        size[2] / 2,
+    )
+
+    logger.info(location, size)
+    return location, size
+
+
+### Obstacle:
+def extract_obstacle_from_log(log_address: str, size: Position = None):
+    """extracts and returns detected obstacle borders from the input log"""
+
+    avoidance_active_periods = extract_collission_prevension_active_periods(log_address)
+
+    points: List[Position] = []
+    idx = 0
+
+    obstacle_distance = file_helper.extract(log_address, "obstacle_distance_fused")
+    # dropping duplicate rows (some rows are repated in the logs with the same timestamps)
+    obstacle_distance.drop_duplicates("timestamp", inplace=True)
+    for i, row in obstacle_distance.iterrows():
+        tmstmp = int(row["timestamp"])
+        if tmstmp < avoidance_active_periods[idx][0]:
+            continue
+        elif tmstmp <= avoidance_active_periods[idx][1]:
+            p = Position(
+                None,
+                None,
+                None,
+                timestamp=row.timestamp,
+            )
+            p.distance = (
+                float(row["distances[0]"]) / 100
+            )  # convert to meters (originally cm)
+            points.append(p)
+
+        else:
+            idx += 1
+            if idx >= len(avoidance_active_periods):
+                break
+        # logger.info (points)
+
+    trajectory = Trajectory.extract_from_log(log_address)
+    trj_times = [p.timestamp for p in trajectory.positions]
+    for p in points:
+        # find nearest data in local positions
+        idx = timeserie_helper.find_nearest_index(trj_times, p.timestamp)
+
+        # updating position based on yaw angle
+        obstacle_point = trajectory.positions[idx].get_position_in_relative_distance(
+            p.distance
+        )
+
+        p.x, p.y, p.z, p.r = (
+            obstacle_point.x,
+            obstacle_point.y,
+            obstacle_point.z,
+            obstacle_point.r,
+        )
+
+    logger.info(points)
+    return estimate_box_pose(points, size=size)
+
+
+### Trajectory
+def extract_collission_prevension_active_periods(log_address):
+    collision_constraints = file_helper.extract(log_address, "collision_constraints")
+    if collision_constraints is None:
+        return []
+    avoidance_active_periods = []
+    period_start = -1
+    for ind, row in collision_constraints.iterrows():
+        # skip the rows where CP did not change setpoints (where in-active)
+        if (
+            row["original_setpoint[0]"] != row["adapted_setpoint[0]"]
+            or row["original_setpoint[1]"] != row["adapted_setpoint[1]"]
+        ):
+            if period_start < 0:
+                period_start = int(row["timestamp"])
+                avoidance_active_periods.append((period_start, int(row["timestamp"])))
+            else:
+                avoidance_active_periods[-1] = (
+                    period_start,
+                    int(row["timestamp"]),
+                )
+        else:
+            period_start = -1
+
+    return avoidance_active_periods
diff --git a/aerialist/px4/trajectory.py b/aerialist/px4/trajectory.py
index 876c2b5..09b15c9 100644
--- a/aerialist/px4/trajectory.py
+++ b/aerialist/px4/trajectory.py
@@ -10,10 +10,11 @@
 import ruptures as rpt
 from shapely.geometry import LineString
 from decouple import config
+from tslearn.barycenters import softdtw_barycenter
+import warnings
 from .obstacle import Obstacle
 from .position import Position
 from . import file_helper, timeserie_helper
-from tslearn.barycenters import softdtw_barycenter
 
 
 class Trajectory(object):
@@ -23,6 +24,7 @@ class Trajectory(object):
     IGNORE_AUTO_MODES = False
     REMOVE_OFFSET = True
     PLOT_TESTS_XYZ = config("PLOT_TESTS_XYZ", default=True, cast=bool)
+    PLOT_R = config("PLOT_R", default=False, cast=bool)
     TIME_RANGE = None
     DISTANCE_METHOD = config("DISTANCE_METHOD", default="dtw")
     AVERAGE_METHOD = config("AVERAGE_METHOD", default="dtw")
@@ -31,13 +33,11 @@ class Trajectory(object):
     SAMPLING_PERIOD = config("TRJ_SMPL_PRD", cast=float, default=500000)
     RESAMPLE = config("RESAMPLE", default=True, cast=bool)
     AVE_CUT_LAND = config("AVE_CUT_LAND", default=True, cast=bool)
+    HIGHLIGHT_COLOR = "red"
 
-    def __init__(
-        self, positions: List[Position], highlights: List[tuple[int, int]] = []
-    ) -> None:
+    def __init__(self, positions: List[Position]) -> None:
         super().__init__()
         self.positions = positions
-        self.highlights = highlights
 
     def plot(
         self,
@@ -45,12 +45,13 @@ def plot(
         save: bool = True,
         obstacles: List[Obstacle] = None,
         file_prefix="",
+        highlights: List[float] = None,
     ):
         distance = None
         if goal is not None:
             distance = self.distance(goal)
         self.plot_multiple(
-            [self], goal, save, distance, self.highlights, obstacles, file_prefix
+            [self], goal, save, distance, highlights, obstacles, file_prefix
         )
 
     def save_csv(self, address: str) -> None:
@@ -65,13 +66,18 @@ def plot_multiple(
         goal: Trajectory = None,
         save: bool = True,
         distance: float | bool = None,
-        highlights: bool = None,
+        highlights: List[float] = None,
         obstacles: List[Obstacle] = None,
         file_prefix="",
         ave_trajectory: Trajectory = None,
     ):
         fig = plt.figure(tight_layout=True)
-        gs = fig.add_gridspec(3, 4)
+
+        if cls.PLOT_R:
+            gs = fig.add_gridspec(4, 4)
+            r_plt = fig.add_subplot(gs[3, :2])
+        else:
+            gs = fig.add_gridspec(3, 4)
         x_plt = fig.add_subplot(gs[0, :2])
         y_plt = fig.add_subplot(gs[1, :2])
         z_plt = fig.add_subplot(gs[2, :2])
@@ -84,7 +90,11 @@ def plot_multiple(
         x_plt.set_ylabel("X (m)")
         y_plt.set_ylabel("Y (m)")
         z_plt.set_ylabel("Z (m)")
-        z_plt.set_xlabel("flight time (s)")
+        if cls.PLOT_R:
+            r_plt.set_ylabel("Yaw (\u00b0)")
+            r_plt.set_xlabel("flight time (s)")
+        else:
+            z_plt.set_xlabel("flight time (s)")
         xy_plt.set_ylabel("Y (m)")
         xy_plt.yaxis.set_label_position("right")
         xy_plt.yaxis.tick_right()
@@ -100,38 +110,6 @@ def plot_multiple(
                     labeled = True
                 xy_plt.add_patch(obst_patch)
 
-                # obst_patch = mpatches.Rectangle(
-                #     (
-                #         obstacle[0][0] - obstacle[1][0] / 2,
-                #         obstacle[0][2] - obstacle[1][2] / 2,
-                #     ),
-                #     obstacle[1][0],
-                #     obstacle[1][2],
-                #     edgecolor="black",
-                #     fill=False,
-                #     alpha=0.5,
-                # )
-                # xyz_plt.add_patch(obst_patch)
-                # mplot3d.art3d.pathpatch_2d_to_3d(
-                #     obst_patch, z=obstacle[0][1] - obstacle[1][1] / 2, zdir="y"
-                # )
-
-                # patch2 = mpatches.Rectangle(
-                #     (
-                #         obstacle[0][0] - obstacle[1][0] / 2,
-                #         obstacle[0][2] - obstacle[1][2] / 2,
-                #     ),
-                #     obstacle[1][0],
-                #     obstacle[1][2],
-                #     edgecolor="black",
-                #     fill=False,
-                #     alpha=0.5,
-                # )
-                # xyz_plt.add_patch(patch2)
-                # mplot3d.art3d.pathpatch_2d_to_3d(
-                #     patch2, z=obstacle[0][1] + obstacle[1][1] / 2, zdir="y"
-                # )
-
         alpha = 1 if len(trajectories) <= 1 else 0.25
         for i in range(len(trajectories)):
             data_frame = trajectories[i].to_data_frame()
@@ -140,12 +118,12 @@ def plot_multiple(
                 x_plt.plot(data_frame[:, 0], data_frame[:, 1], alpha=alpha)
                 y_plt.plot(data_frame[:, 0], data_frame[:, 2], alpha=alpha)
                 z_plt.plot(data_frame[:, 0], data_frame[:, 3], alpha=alpha)
+                if cls.PLOT_R:
+                    r_plt.plot(data_frame[:, 0], data_frame[:, 4], alpha=alpha)
+
             label = None
             if i == 0:
-                # if len(trajectories) > 1:
                 label = "tests"
-                # elif goal is None:
-                #     label = "flight trajectory"
 
             xy_plt.plot(
                 data_frame[:, 1],
@@ -153,6 +131,14 @@ def plot_multiple(
                 label=label,
                 alpha=alpha,
             )
+            if highlights is not None:
+                for timestamp in highlights:
+                    point = data_frame[
+                        abs(data_frame[:, 0] - (timestamp / 1000000.0)).argsort()[0]
+                    ]
+                    xy_plt.scatter(
+                        [point[1]], [point[2]], color=cls.HIGHLIGHT_COLOR, alpha=0.3
+                    )
             # xyz_plt.plot3D(
             #     [p.x for p in trj.positions],
             #     [p.y for p in trj.positions],
@@ -166,22 +152,23 @@ def plot_multiple(
             x_plt.plot(
                 data_frame[:, 0], data_frame[:, 1], label="test ave.", color="red"
             )
-
             y_plt.plot(data_frame[:, 0], data_frame[:, 2], color="red")
-
             z_plt.plot(data_frame[:, 0], data_frame[:, 3], color="red")
-
+            if cls.PLOT_R:
+                r_plt.plot(data_frame[:, 0], data_frame[:, 4], color="red")
             xy_plt.plot(data_frame[:, 1], data_frame[:, 2], color="red")
+        else:
+            ave_trajectory = trajectories[0]
 
         if goal != None:
             data_frame = goal.to_data_frame()
             x_plt.plot(
                 data_frame[:, 0], data_frame[:, 1], label="original", color="blue"
             )
-
             y_plt.plot(data_frame[:, 0], data_frame[:, 2], color="blue")
-
             z_plt.plot(data_frame[:, 0], data_frame[:, 3], color="blue")
+            if cls.PLOT_R:
+                r_plt.plot(data_frame[:, 0], data_frame[:, 4], color="blue")
 
             xy_plt.plot(data_frame[:, 1], data_frame[:, 2], color="blue")
 
@@ -190,12 +177,12 @@ def plot_multiple(
             #     [p.y for p in goal.positions],
             #     [p.z for p in goal.positions],
             # )
-        else:
-            ave_trajectory = trajectories[0]
 
+        if distance == True and obstacles is not None:
+            distance = ave_trajectory.distance_to_obstacles(obstacles)
+        else:
+            distance = None
         if distance is not None:
-            if distance == True and goal is not None:
-                distance = goal.distance(ave_trajectory)
             fig.text(
                 0.5,
                 0.03,
@@ -203,31 +190,37 @@ def plot_multiple(
                 ha="center",
                 bbox=dict(facecolor="none", edgecolor="lightgray", boxstyle="round"),
             )
-
         if highlights is not None:
-            for period in highlights:
-                x_plt.axvspan(
-                    period[0] / 1000000.0,
-                    period[1] / 1000000.0,
-                    color="yellow",
-                    alpha=0.5,
+            for timestamp in highlights:
+                x_plt.axvline(
+                    timestamp / 1000000.0,
+                    color=cls.HIGHLIGHT_COLOR,
+                    alpha=0.3,
+                    linewidth=1.75,
                 )
-                y_plt.axvspan(
-                    period[0] / 1000000.0,
-                    period[1] / 1000000.0,
-                    color="yellow",
-                    alpha=0.5,
+                y_plt.axvline(
+                    timestamp / 1000000.0,
+                    color=cls.HIGHLIGHT_COLOR,
+                    alpha=0.3,
+                    linewidth=1.75,
                 )
-                z_plt.axvspan(
-                    period[0] / 1000000.0,
-                    period[1] / 1000000.0,
-                    color="yellow",
-                    alpha=0.5,
+                z_plt.axvline(
+                    timestamp / 1000000.0,
+                    color=cls.HIGHLIGHT_COLOR,
+                    alpha=0.3,
+                    linewidth=1.75,
                 )
+                if cls.PLOT_R:
+                    r_plt.axvline(
+                        timestamp / 1000000.0,
+                        color=cls.HIGHLIGHT_COLOR,
+                        alpha=0.3,
+                        linewidth=1.75,
+                    )
 
         fig.legend(loc="upper center", ncol=3 if obstacles is None else 4)
         if save:
-            filename = file_prefix + file_helper.time_filename()
+            filename = file_prefix + file_helper.time_filename(add_host=True)
             os.makedirs(cls.DIR, exist_ok=True)
             fig.savefig(f"{cls.DIR}{filename}.png")
             plt.close(fig)
@@ -244,8 +237,10 @@ def allign_origin(self):
             p.x -= origin.x
             p.y -= origin.y
             p.z -= origin.z
-            p.timestamp -= origin.timestamp
-            p.r -= origin.r
+            if origin.timestamp is not None:
+                p.timestamp -= origin.timestamp
+            if origin.r is not None:
+                p.r -= origin.r
 
     def distance(self, other: Trajectory) -> float:
         """quantify the difference between the two trajectoryies using Dynamic Time Warping and normalized datapoints"""
@@ -254,16 +249,14 @@ def distance(self, other: Trajectory) -> float:
             return self.dtw_t_distance(other)
         elif self.DISTANCE_METHOD == "frechet":
             return self.frechet_distance(other)
-        if self.DISTANCE_METHOD == "dtw-tweaked":
-            return self.dtw_tweaked_distance(other)
         else:
             return self.dtw_distance(other)
 
     def dtw_t_distance(self, other: Trajectory) -> float:
         """quantify the difference between the two trajectoryies using Dynamic Time Warping and normalized datapoints"""
         dtw, d = similaritymeasures.dtw(
-            timeserie_helper.normalize(self.to_data_frame()),
-            timeserie_helper.normalize(other.to_data_frame()),
+            timeserie_helper.normalize(self.to_data_frame()[:, 0:4]),
+            timeserie_helper.normalize(other.to_data_frame()[:, 0:4]),
         )
         return dtw
 
@@ -271,24 +264,15 @@ def dtw_distance(self, other: Trajectory) -> float:
         """quantify the difference between the two trajectories using Dynamic Time Warping and not considering the timestamps"""
 
         dtw, d = similaritymeasures.dtw(
-            self.to_data_frame()[:, 1:],
-            other.to_data_frame()[:, 1:],
-        )
-        return dtw
-
-    def dtw_tweaked_distance(self, other: Trajectory) -> float:
-        """quantify the difference between the two trajectories using Dynamic Time Warping and not considering the timestamps"""
-
-        dtw, d = timeserie_helper.dtw_tweaked(
-            self.to_data_frame()[:, 1:],
-            other.to_data_frame()[:, 1:],
+            self.to_data_frame()[:, 1:4],
+            other.to_data_frame()[:, 1:4],
         )
         return dtw
 
     def frechet_distance(self, other: Trajectory) -> float:
         """quantify the difference between the two trajectories using Frechete and not considering the timestamps"""
         d = similaritymeasures.frechet_dist(
-            self.to_data_frame()[:, 1:], other.to_data_frame()[:, 1:]
+            self.to_data_frame()[:, 1:4], other.to_data_frame()[:, 1:4]
         )
         return d
 
@@ -303,12 +287,13 @@ def to_data_frame(self):
             ]
 
         data = np.zeros(
-            (len(positions), 4),
+            (len(positions), 5),
         )
         data[:, 0] = [p.timestamp / 1000000.0 for p in positions]
         data[:, 1] = [p.x for p in positions]
         data[:, 2] = [p.y for p in positions]
         data[:, 3] = [p.z for p in positions]
+        data[:, 4] = [p.r for p in positions]
         return data
 
     def downsample(self, count: int) -> Trajectory:
@@ -382,7 +367,6 @@ def extract_from_log(
         log_address: str,
         ignore_automodes=False,
         is_jmavsim=False,
-        load_CP_activations=True,
     ):
         """extracts and returns trajectory logs from the input log"""
         positions: List[Position] = []
@@ -408,16 +392,16 @@ def extract_from_log(
             local_position = file_helper.extract(log_address, "vehicle_local_position")
             local_position = local_position[["timestamp", "x", "y", "z", "heading"]]
             for row in local_position.itertuples():
-                positions.append(
-                    Position(
-                        row.x,
-                        row.y,
-                        -row.z,
-                        row.heading,
-                        timestamp=row.timestamp,
-                        is_jmavsim=is_jmavsim,
-                    )
+                pos = Position(
+                    row.x,
+                    row.y,
+                    -row.z,
+                    row.heading,
+                    timestamp=row.timestamp,
                 )
+                if is_jmavsim:
+                    pos.convert_jmavsim()
+                positions.append(pos)
 
         if cls.REMOVE_OFFSET:
             offset = positions[0].timestamp
@@ -465,15 +449,94 @@ def extract_from_log(
 
             positions = filtered_positions
 
-        cp_activations = None
-        if load_CP_activations:
-            cp_activations = cls.extract_CP_active_periods(log_address)
-
-        traj = cls(positions, cp_activations)
+        traj = cls(positions)
         if cls.RESAMPLE:
             traj = traj.downsample_time()
         return traj
 
+    @classmethod
+    def extract_waypoints(cls, log_address: str):
+        """extracts and returns trajectory logs from the input log"""
+        positions: List[Position] = []
+
+        # waypoints
+        waypoints = file_helper.extract(log_address, "vehicle_trajectory_waypoint")
+        waypoints = waypoints[
+            [
+                "timestamp",
+                "waypoints[0].position[0]",
+                "waypoints[0].position[1]",
+                "waypoints[0].position[2]",
+                "waypoints[0].yaw",
+            ]
+        ]
+        waypoints.columns = ["timestamp", "x", "y", "z", "heading"]
+        for row in waypoints.itertuples():
+            pos = Position(
+                row.x,
+                row.y,
+                -row.z,
+                row.heading,
+                timestamp=row.timestamp,
+            )
+            positions.append(pos)
+
+        if cls.REMOVE_OFFSET:
+            offset = positions[0].timestamp
+            for p in positions:
+                p.timestamp -= offset
+
+        if cls.ALLIGN_ORIGIN:
+            origin = copy.deepcopy(positions[0])
+            for p in positions:
+                p.x -= origin.x
+                p.y -= origin.y
+                p.z -= origin.z
+                p.r -= origin.r
+
+        traj = cls(positions)
+        # if cls.RESAMPLE:
+        #     traj = traj.downsample_time()
+        return traj
+
+    @classmethod
+    def extract_groundtruth(cls, log_address: str):
+        """extracts and returns trajectory logs from the input log"""
+        positions: List[Position] = []
+
+        # waypoints
+        waypoints = file_helper.extract(
+            log_address, "vehicle_local_position_groundtruth"
+        )
+        waypoints = waypoints[["timestamp", "x", "y", "z", "heading"]]
+        for row in waypoints.itertuples():
+            pos = Position(
+                row.x,
+                row.y,
+                -row.z,
+                row.heading,
+                timestamp=row.timestamp,
+            )
+            positions.append(pos)
+
+        if cls.REMOVE_OFFSET:
+            offset = positions[0].timestamp
+            for p in positions:
+                p.timestamp -= offset
+
+        if cls.ALLIGN_ORIGIN:
+            origin = copy.deepcopy(positions[0])
+            for p in positions:
+                p.x -= origin.x
+                p.y -= origin.y
+                p.z -= origin.z
+                p.r -= origin.r
+
+        traj = cls(positions)
+        # if cls.RESAMPLE:
+        #     traj = traj.downsample_time()
+        return traj
+
     @classmethod
     def get_home(cls, log_address: str):
         global_position = file_helper.extract(log_address, "vehicle_global_position")
@@ -497,36 +560,6 @@ def extract_segments(self) -> List[Trajectory]:
 
         return segments
 
-    @classmethod
-    def extract_CP_active_periods(cls, log_address):
-        collision_constraints = file_helper.extract(
-            log_address, "collision_constraints"
-        )
-        if collision_constraints is None:
-            return []
-        avoidance_active_periods = []
-        period_start = -1
-        for ind, row in collision_constraints.iterrows():
-            # skip the rows where CP did not change setpoints (where in-active)
-            if (
-                row["original_setpoint[0]"] != row["adapted_setpoint[0]"]
-                or row["original_setpoint[1]"] != row["adapted_setpoint[1]"]
-            ):
-                if period_start < 0:
-                    period_start = int(row["timestamp"])
-                    avoidance_active_periods.append(
-                        (period_start, int(row["timestamp"]))
-                    )
-                else:
-                    avoidance_active_periods[-1] = (
-                        period_start,
-                        int(row["timestamp"]),
-                    )
-            else:
-                period_start = -1
-
-        return avoidance_active_periods
-
     @classmethod
     def average(cls, trajectories: List[Trajectory]) -> Trajectory:
         if cls.AVERAGE_METHOD == "dtw":
@@ -548,12 +581,17 @@ def simple_average(cls, trajectories: List[Trajectory]) -> Trajectory:
 
     @classmethod
     def dtw_average(cls, trajectories: List[Trajectory]) -> Trajectory:
-        # resampled = [t.downsample_time() for t in trajectories]
-        resampled = trajectories
-        dataset = [t.to_data_frame()[:, 1:] for t in resampled]
-        average_data = softdtw_barycenter(dataset, gamma=cls.AVE_GAMMA)
-        time_dataset = [t.to_data_frame()[:, 0] for t in resampled]
-        average_time = softdtw_barycenter(time_dataset, gamma=cls.AVE_GAMMA)
+        with warnings.catch_warnings():
+            warnings.simplefilter("ignore")
+            # resampled = [t.downsample_time() for t in trajectories]
+            resampled = trajectories
+            dataset = [t.to_data_frame()[:, 1:4] for t in resampled]
+            average_data = softdtw_barycenter(dataset, gamma=cls.AVE_GAMMA)
+            time_dataset = [t.to_data_frame()[:, 0] for t in resampled]
+            average_time = softdtw_barycenter(time_dataset, gamma=cls.AVE_GAMMA)
+            r_dataset = [t.to_data_frame()[:, 4] for t in resampled]
+            average_r = softdtw_barycenter(r_dataset, gamma=cls.AVE_GAMMA)
+
         ave_positions: List[Position] = []
         if len(average_time) == len(average_data):
             for i in range(len(average_time)):
@@ -563,6 +601,7 @@ def dtw_average(cls, trajectories: List[Trajectory]) -> Trajectory:
                         x=average_data[i, 0],
                         y=average_data[i, 1],
                         z=average_data[i, 2],
+                        r=average_r[i, 0],
                     )
                 )
         ave_trj = Trajectory(ave_positions)
diff --git a/aerialist/resources/k8s/k8s-job-avoidance-local.yaml b/aerialist/resources/k8s/k8s-job-avoidance-local.yaml
new file mode 100755
index 0000000..685d652
--- /dev/null
+++ b/aerialist/resources/k8s/k8s-job-avoidance-local.yaml
@@ -0,0 +1,70 @@
+apiVersion: batch/v1
+kind: Job
+metadata:
+  name: batch-test
+  labels:
+    {}
+spec:
+  selector:
+    matchLabels:
+  template:
+    metadata:
+      labels:
+        {}
+    spec:
+      volumes:
+        - name: host-path-volume
+          hostPath:
+            path: VOLUME
+        - configMap:
+            defaultMode: 420
+            name: k8s-config
+          name: vol-k8s-conf
+      containers:
+        - name: init
+          image: skhatiri/aerialist:k8s
+          imagePullPolicy: Always
+          stdin: true
+          stdinOnce: false
+          command:
+            - /bin/bash
+            - -c
+            - /bin/sleep 20  && kubectl exec -it $HOSTNAME -c sim -- /bin/bash -c "$(COMMAND) && pkill bash"
+          env:
+            - name: COMMAND
+              value: "python3 aerialist --help"
+          volumeMounts:
+            - mountPath: /root/.kube/config
+              name: vol-k8s-conf
+              readOnly: true
+              subPath: k8s-config.yaml
+            - name: host-path-volume
+              mountPath: /src/aerialist/results
+          tty: true
+        - name: sim
+          image: skhatiri/aerialist
+          imagePullPolicy: Always
+          stdin: true
+          stdinOnce: true
+          command: 
+          tty: true            
+          volumeMounts:
+            - mountPath: /root/.kube/config
+              name: vol-k8s-conf
+              readOnly: true
+              subPath: k8s-config.yaml
+            - name: host-path-volume
+              mountPath: /src/aerialist/results
+      affinity:
+      imagePullSecrets:
+        - name: dockerhub
+      initContainers:
+      restartPolicy: Never
+      activeDeadlineSeconds: 600
+  completions: 1
+  suspend: false
+  # activeDeadlineSeconds: 1000
+  backoffLimit: 1
+  completionMode: NonIndexed
+  parallelism: 1
+  ttlSecondsAfterFinished: 60
\ No newline at end of file
diff --git a/aerialist/resources/k8s/k8s-job-avoidance.yaml b/aerialist/resources/k8s/k8s-job-avoidance.yaml
index 1884db8..ab30d63 100755
--- a/aerialist/resources/k8s/k8s-job-avoidance.yaml
+++ b/aerialist/resources/k8s/k8s-job-avoidance.yaml
@@ -20,7 +20,6 @@ spec:
       containers:
         - imagePullPolicy: Always
           name: init
-          __active: true
           image: skhatiri/aerialist:k8s
           stdin: true
           stdinOnce: false
@@ -44,7 +43,6 @@ spec:
             requests: {}
         - imagePullPolicy: Always
           name: sim
-          __active: true
           image: skhatiri/aerialist
           stdin: true
           stdinOnce: true
@@ -55,12 +53,7 @@ spec:
               valueFrom:
                 secretKeyRef:
                   name: webdav
-                  key: host        
-            - name: WEBDAV_ROOT
-              valueFrom:
-                secretKeyRef:
-                  name: webdav
-                  key: root               
+                  key: host                  
             - name: WEBDAV_USER
               valueFrom:
                 secretKeyRef:
@@ -86,11 +79,8 @@ spec:
       activeDeadlineSeconds: 600
   completions: 1
   suspend: false
-  replicas: 1
   # activeDeadlineSeconds: 1000
   backoffLimit: 5
   completionMode: NonIndexed
-#  manualSelector: boolean
   parallelism: 1
-  ttlSecondsAfterFinished: 60
-__clone: true
\ No newline at end of file
+  ttlSecondsAfterFinished: 60
\ No newline at end of file
diff --git a/aerialist/resources/k8s/k8s-job-avoidance2.yaml b/aerialist/resources/k8s/k8s-job-avoidance2.yaml
new file mode 100755
index 0000000..907f4d5
--- /dev/null
+++ b/aerialist/resources/k8s/k8s-job-avoidance2.yaml
@@ -0,0 +1,62 @@
+apiVersion: batch/v1
+kind: Job
+metadata:
+  name: batch-test
+  labels:
+    {}
+spec:
+  selector:
+    matchLabels:
+  template:
+    metadata:
+      labels:
+        {}
+    spec:
+      containers:
+        - imagePullPolicy: Always
+          name: container-0
+          image: skhatiri/aerialist
+          stdin: true
+          stdinOnce: false
+          command:
+            - /bin/bash
+            - -c
+            - "$(COMMAND)" 
+          tty: true
+          env:
+            - name: WEBDAV_HOST
+              valueFrom:
+                secretKeyRef:
+                  name: webdav
+                  key: host          
+            - name: WEBDAV_USER
+              valueFrom:
+                secretKeyRef:
+                  name: webdav
+                  key: user           
+            - name: WEBDAV_PASS
+              valueFrom:
+                secretKeyRef:
+                  name: webdav
+                  key: pass
+            - name: COMMAND
+              value: "python3 aerialist --help"          
+          resources:
+            requests:
+              cpu: 6000m
+              memory: 2000Mi
+            limits:
+              cpu: 6000m
+              memory: 4000Mi
+      affinity:
+      imagePullSecrets:
+        - name: dockerhub
+      initContainers:
+      restartPolicy: Never
+  completions: 1
+  suspend: false
+#  activeDeadlineSeconds: int
+  backoffLimit: 5
+  completionMode: NonIndexed
+  parallelism: 1
+  ttlSecondsAfterFinished: 60
\ No newline at end of file
diff --git a/aerialist/resources/k8s/k8s-job-local.yaml b/aerialist/resources/k8s/k8s-job-local.yaml
new file mode 100755
index 0000000..3aa411b
--- /dev/null
+++ b/aerialist/resources/k8s/k8s-job-local.yaml
@@ -0,0 +1,53 @@
+apiVersion: batch/v1
+kind: Job
+metadata:
+  name: batch-test
+  labels:
+    {}
+spec:
+  selector:
+    matchLabels:
+  template:
+    metadata:
+      labels:
+        {}
+    spec:
+      volumes:
+        - name: host-path-volume
+          hostPath:
+            path: VOLUME
+      containers:
+        - imagePullPolicy: Always
+          name: container-0
+          image: skhatiri/aerialist
+          stdin: true
+          stdinOnce: false
+          command:
+            - /bin/bash
+            - -c
+            - "$(COMMAND)" 
+          tty: true
+          env:
+            - name: COMMAND
+              value: "python3 aerialist --help"
+          volumeMounts:
+            - name: host-path-volume
+              mountPath: /src/aerialist/results
+          resources:
+            requests:
+              cpu: 750m
+              memory: 300Mi
+            limits:
+              cpu: 750m
+              memory: 300Mi
+      affinity:
+      imagePullSecrets:
+        - name: dockerhub
+      initContainers:
+      restartPolicy: Never
+  completions: 1
+  suspend: false
+  backoffLimit: 5
+  completionMode: NonIndexed
+  parallelism: 1
+  ttlSecondsAfterFinished: 60
diff --git a/aerialist/resources/k8s/k8s-job.yaml b/aerialist/resources/k8s/k8s-job.yaml
index efe545e..8c553b7 100755
--- a/aerialist/resources/k8s/k8s-job.yaml
+++ b/aerialist/resources/k8s/k8s-job.yaml
@@ -15,7 +15,6 @@ spec:
       containers:
         - imagePullPolicy: Always
           name: container-0
-          __active: true
           image: skhatiri/aerialist
           stdin: true
           stdinOnce: false
@@ -29,12 +28,7 @@ spec:
               valueFrom:
                 secretKeyRef:
                   name: webdav
-                  key: host        
-            - name: WEBDAV_ROOT
-              valueFrom:
-                secretKeyRef:
-                  name: webdav
-                  key: root        
+                  key: host            
             - name: WEBDAV_USER
               valueFrom:
                 secretKeyRef:
@@ -55,7 +49,6 @@ spec:
             limits:
               cpu: 750m
               memory: 300Mi
-            requests: {}
       affinity:
       imagePullSecrets:
         - name: dockerhub
@@ -63,11 +56,8 @@ spec:
       restartPolicy: Never
   completions: 1
   suspend: false
-  replicas: 1
 #  activeDeadlineSeconds: int
   backoffLimit: 5
   completionMode: NonIndexed
-#  manualSelector: boolean
   parallelism: 1
-  ttlSecondsAfterFinished: 60
-__clone: true
\ No newline at end of file
+  ttlSecondsAfterFinished: 60
\ No newline at end of file
diff --git a/aerialist/resources/k8s/k8s-pod-avoidance.yaml b/aerialist/resources/k8s/k8s-pod-avoidance.yaml
index 9135603..99ae650 100755
--- a/aerialist/resources/k8s/k8s-pod-avoidance.yaml
+++ b/aerialist/resources/k8s/k8s-pod-avoidance.yaml
@@ -9,7 +9,6 @@ spec:
   containers:
     - imagePullPolicy: Always
       name: sim
-      __active: true
       image: skhatiri/aerialist
       stdin: true
       stdinOnce: true
@@ -41,17 +40,16 @@ spec:
         requests: {}
     - imagePullPolicy: Always
       name: init
-      __active: true
       image: skhatiri/aerialist:k8s
       stdin: true
       stdinOnce: false
       command:
-        - /bin/bash 
-        - -c 
-        - /bin/sleep 5 && kubectl exec -it $HOSTNAME -c sim -- /bin/bash -c "$(COMMAND) && pkill bash"
+        # - /bin/bash 
+        # - -c 
+        # - /bin/sleep 5 && kubectl exec -it $HOSTNAME -c sim -- /bin/bash -c "$(COMMAND) && pkill bash"
       env:
         - name: COMMAND
-          value: "timeout 600 python3 aerialist exec --obst 15 0 2.5 1 10 5 --cloud --mission resources/logs/m0.plan --params resources/simulation/params_avoidance.csv --output ASE2022/obst/ --drone ros --env avoidance --headless experiment replay"
+          value: "python3 aerialist --help"
       tty: true
       resources:
         limits:
diff --git a/aerialist/resources/k8s/k8s-pod-master.yaml b/aerialist/resources/k8s/k8s-pod-master.yaml
index 39f5c1c..ad01071 100755
--- a/aerialist/resources/k8s/k8s-pod-master.yaml
+++ b/aerialist/resources/k8s/k8s-pod-master.yaml
@@ -13,7 +13,6 @@ spec:
   containers:
     - imagePullPolicy: Always
       name: init
-      __active: true
       image: skhatiri/aerialist:k8s
       stdin: true
       stdinOnce: false
@@ -28,12 +27,7 @@ spec:
           valueFrom:
             secretKeyRef:
               name: webdav
-              key: host        
-        - name: WEBDAV_ROOT
-          valueFrom:
-            secretKeyRef:
-              name: webdav
-              key: root               
+              key: host                     
         - name: WEBDAV_USER
           valueFrom:
             secretKeyRef:
diff --git a/aerialist/resources/simulation/collision_prevention.launch b/aerialist/resources/simulation/collision_prevention.launch
index 129be5f..a2bfa53 100644
--- a/aerialist/resources/simulation/collision_prevention.launch
+++ b/aerialist/resources/simulation/collision_prevention.launch
@@ -23,6 +23,24 @@
     <arg name="obst2_l" default = "1"/>
     <arg name="obst2_w" default = "1"/>
     <arg name="obst2_h" default = "1"/>
+
+    <arg name="obst3" default = "false"/>
+    <arg name="obst3_x" default = "0"/>
+    <arg name="obst3_y" default = "0"/>
+    <arg name="obst3_z" default = "0"/>
+    <arg name="obst3_yaw" default = "0"/>
+    <arg name="obst3_l" default = "1"/>
+    <arg name="obst3_w" default = "1"/>
+    <arg name="obst3_h" default = "1"/>
+
+    <arg name="obst4" default = "false"/>
+    <arg name="obst4_x" default = "0"/>
+    <arg name="obst4_y" default = "0"/>
+    <arg name="obst4_z" default = "0"/>
+    <arg name="obst4_yaw" default = "0"/>
+    <arg name="obst4_l" default = "1"/>
+    <arg name="obst4_w" default = "1"/>
+    <arg name="obst4_h" default = "1"/>
     
     <arg name="ns" default="/"/>
     <arg name="model" default="iris_depth_camera"/>
@@ -35,6 +53,41 @@
     <param name="use_sim_time" value="true" />
 
 
+
+    <!-- add obstacles -->
+    <group if="$(arg obst)">
+        <!-- Convert xacro and put on parameter server -->
+        <param name="box_description" command="$(find xacro)/xacro.py aerialist/resources/simulation/box.xacro name:=box l:=$(arg obst_l) w:=$(arg obst_w) h:=$(arg obst_h)" />
+        
+        <node name="spawn_obstacle" pkg="gazebo_ros" type="spawn_model" output="screen"
+            args="-param box_description -urdf -x $(arg obst_x) -y $(arg obst_y) -z $(arg obst_z) -Y $(arg obst_yaw) -model box/">
+        </node>
+    </group>
+    <group if="$(arg obst2)">
+        <!-- Convert xacro and put on parameter server -->
+        <param name="box_description2" command="$(find xacro)/xacro.py aerialist/resources/simulation/box.xacro name:=box2 l:=$(arg obst2_l) w:=$(arg obst2_w) h:=$(arg obst2_h)" />
+        
+        <node name="spawn_obstacle2" pkg="gazebo_ros" type="spawn_model" output="screen"
+            args="-param box_description2 -urdf -x $(arg obst2_x) -y $(arg obst2_y) -z $(arg obst2_z) -Y $(arg obst2_yaw) -model box2/">
+        </node>
+    </group>
+    <group if="$(arg obst3)">
+        <!-- Convert xacro and put on parameter server -->
+        <param name="box_description3" command="$(find xacro)/xacro.py aerialist/resources/simulation/box.xacro name:=box3 l:=$(arg obst3_l) w:=$(arg obst3_w) h:=$(arg obst3_h)" />
+        
+        <node name="spawn_obstacle3" pkg="gazebo_ros" type="spawn_model" output="screen"
+            args="-param box_description3 -urdf -x $(arg obst3_x) -y $(arg obst3_y) -z $(arg obst3_z) -Y $(arg obst3_yaw) -model box3/">
+        </node>
+    </group>
+    <group if="$(arg obst4)">
+        <!-- Convert xacro and put on parameter server -->
+        <param name="box_description4" command="$(find xacro)/xacro.py aerialist/resources/simulation/box.xacro name:=box4 l:=$(arg obst4_l) w:=$(arg obst4_w) h:=$(arg obst4_h)" />
+        
+        <node name="spawn_obstacle4" pkg="gazebo_ros" type="spawn_model" output="screen"
+            args="-param box_description4 -urdf -x $(arg obst4_x) -y $(arg obst4_y) -z $(arg obst4_z) -Y $(arg obst4_yaw) -model box4/">
+        </node>
+    </group>
+
     <!-- Define a static transform from a camera internal frame to the fcu for every camera used -->
     <node pkg="tf" type="static_transform_publisher" name="tf_depth_camera"
           args="0 0 0 -1.57 0 -1.57 fcu camera_link 10"/>
@@ -71,24 +124,7 @@
     <node name="spawn_model" pkg="gazebo_ros" type="spawn_model" output="screen"
           args="-sdf -database $(arg model) -model $(arg vehicle)">
     </node>
-
-    <!-- add obstacles -->
-    <group if="$(arg obst)">
-        <!-- Convert xacro and put on parameter server -->
-        <param name="box_description" command="$(find xacro)/xacro.py aerialist/resources/simulation/box.xacro name:=box l:=$(arg obst_l) w:=$(arg obst_w) h:=$(arg obst_h)" />
-        
-        <node name="spawn_obstacle" pkg="gazebo_ros" type="spawn_model" output="screen"
-            args="-param box_description -urdf -x $(arg obst_x) -y $(arg obst_y) -z $(arg obst_z) -Y $(arg obst_yaw) -model box/">
-        </node>
-    </group>
-    <group if="$(arg obst2)">
-        <!-- Convert xacro and put on parameter server -->
-        <param name="box_description2" command="$(find xacro)/xacro.py aerialist/resources/simulation/box.xacro name:=box2 l:=$(arg obst2_l) w:=$(arg obst2_w) h:=$(arg obst2_h)" />
-        
-        <node name="spawn_obstacle2" pkg="gazebo_ros" type="spawn_model" output="screen"
-            args="-param box_description2 -urdf -x $(arg obst2_x) -y $(arg obst2_y) -z $(arg obst2_z) -Y $(arg obst2_yaw) -model box2/">
-        </node>
-    </group>
+    
     <!-- Load custom console configuration -->
     <env name="ROSCONSOLE_CONFIG_FILE" value="$(find local_planner)/resource/custom_rosconsole.conf"/>
 
diff --git a/docs/CLI.md b/docs/CLI.md
new file mode 100644
index 0000000..9bd1f24
--- /dev/null
+++ b/docs/CLI.md
@@ -0,0 +1,63 @@
+# Aerialist Command-Line Interface
+
+## Test Execution
+
+You can utilize the toolkit with following command line options:
+
+**Note:** Before running any command, make sure you are at the root directory of the repository:
+
+`cd Aerialist/`
+
+The simplest way to execute a UAV test by Aerialist is by following command:
+`python3 aerialsit exect --test [test-file.yaml]`
+
+More sample tests can be found [here](samples/tests/)
+
+You can use `python3 aerialist exec --help` anywhere to get help on the command parameters.
+
+#### Other CLI Arguments
+
+Alternatively, the above test properties can also be set using commandline aruguments as below.
+
+|argument   | input type            | description                   |
+|-----------|-----------------------|------------------------------ |
+| --test    | path/to/file.yml      | test description yaml file    |
+| --drone   | {**sitl**, ros, cf}   | type of the drone to conect to|
+| --mission | path/to/file.plan     | input mission file address    |
+| --params  | path/to/file.csv      | params file address           |
+| --simulator| {**gazebo**,jmavsim,ros} | the simulator environment to run|
+| --obstacle| float [l,w,h,x,y,z,r] | obstacle size, position, and angle to put in simulation environment|
+| --obstacle2| float [l,w,h,x,y,z,r]| obstacle size, position, and angle to put in simulation environment|  
+| --headless| _                     | whether to run the simulator headless|
+| --speed   | float = 1.0           | the simulator speed relative to real time|
+| --home    | float [lat,lon,alt]   | home position to place the drone|
+| --commands| path/to/file.{ulg,csv}| runtime commands file address |
+| --log     | path/to/file.ulg      | reference log file address    |
+| --agent   |{local,docker,k8s}     | where to run the tests        |
+| -n        | int = 1               | no. of parallel runs          |
+| --path    | path/to/folder/       | cloud output path to copy logs|
+| --id      | string                | k8s job id                    |
+
+Some of the common combination of the following arguments are listed here as sample test cases:
+
+- Replaying a pre-recorded manual flight log:
+
+`python3 aerialist exec --commands samples/flights/manual1.ulg --simulator gazebo --drone sitl`
+
+- Running an existing series of manual commands stored in a csv file. Look [here](samples/flights/manual1-commands.csv) for an example (corresponding to previous .ulg file).
+
+`python3 aerialist exec --commands samples/flights/manual1-commands.csv --simulator gazebo --drone sitl`
+
+- Replaying a pre-recorded manual flight log with collission prevention enabled:
+
+`python3 aerialist exec --commands samples/flights/manual2.ulg --simulator ros --drone ros`
+
+- Executing a pre-planed autonomous flight log with obstacle avoidance enabled:
+
+`python3 aerialist exec --commands samples/flights/mission1-commands.csv --mission samples/flights/mission1.plan --params samples/flights/mission1-params.csv --log samples/flights/mission1.ulg --simulator ros --drone ros`
+
+<!-- - running a manual flight through keyboard commands:
+
+`python3 run.py experiment manual`
+
+Look [here](https://github.com/skhatiri/drone-experiments/blob/5b7950dc99318d08dacab61ea8686c6d65402438/px4/drone.py#L76) for possible commands to send -->
\ No newline at end of file
diff --git a/docs/PX4_installation.md b/docs/PX4_installation.md
new file mode 100644
index 0000000..6e52de9
--- /dev/null
+++ b/docs/PX4_installation.md
@@ -0,0 +1,89 @@
+# PX4 Installation
+
+Installing [PX4-Autopilot](https://github.com/PX4/PX4-Autopilot), [PX4-Avoidance](https://github.com/PX4/PX4-Avoidance) and their requirements including ROS and Gazebo could be problematic in many cases. We only suggest this full installation to the users who needs direct visual access to the simulator or are curious to visually monitor the UAVs during the flight. Othervise, test execution, extractin the flight logs and ploting them can be achieved by the [docker exection](../README.md#dockerized-test-execution) as well.
+
+## Virtual Machine
+
+We have prepared a ready-to-use virtual machine ([download link](https://zhaw-my.sharepoint.com/:f:/g/personal/mazr_zhaw_ch/EnxLqlyju6RMhUYV_SXTqBEBfxundq_-X67eRQAwCPjHvg?e=9953JZ)) to help users onboaord fast. You can move on to using [dockerized test executions](../README.md#dockerized-test-execution) if you don'n need the simulation visualizations anymore.
+
+- Use the VMWare Workstation Player(free version) to import the VM in Windows and Ubuntu and VMWare Fusion(free trial version) to import the VM in MacOS.
+- Make sure that "Accelarate 3D Graphic" under Edit Virtual Machine settings --> Hardware --> Display is unchecked. It might cause Gazebo simulator to crash if selected.
+
+- **Requirements:** Ubuntu 18
+
+## Installation Using bash script
+
+We have prepared a [bash script](../setup_script/px4_avoidance_aerialist_setup.sh) to automate all the steps of installing PX4 and Aerialist in one shot. This has only been tested in a fresh Ubuntu 18.04, so may not fully work for your specific configurations.
+
+We recommend you to use a fresh installed Ubuntu environment to set up Aerialist in order to avoid dependency issues.
+The script was tested in a virtual machine in VMWare.
+You can use VMWare Workstation Player(free version) to set up the clean Ubuntu environment..
+
+### Full Installation
+
+To install PX4, PX4 avoidance along with Aerialist in one shot, download all these three script [px4_avoidance](../setup_script/px4_avoidance_setup.sh), [aerialist](../setup_script/aerialist_setup.sh) and [installation script](../setup_script/px4_avoidance_aerialist_setup.sh) in the same folder and run below command in the terminal:
+
+```
+chmod +x px4_avoidance_aerialist_setup.sh
+./px4_avoidance_aerialist_setup.sh 
+```
+
+Now the installation will continue for around half an hour to one hour depending on your Internet speed.
+
+Once the script has completed successfully, you can close the terminal and open a new terminal.
+In the new terminal, you can see the conda base environment has been now activated.
+
+You can switch to Aerialist environment by typing below command:
+
+```
+conda activate Aerialist
+```
+
+Now navigate into Aerialist directory which is located in your home directory by entering below command in the terminal.
+
+```
+cd ~/Aerialist
+```
+
+### Separate Installation:
+We also provide a separate [script]((../setup_script/px4_avoidance_setup.sh)) to install only PX4 and PX4 avoidance.
+
+Download this script and run below command in the terminal to install:
+
+```
+chmod +x px4_avoidance_setup.sh
+./px4_avoidance_setup.sh
+```
+
+If you also want to install Aerialist, you can download and run [aerialist_setup.sh](../setup_script/aerialist_setup.sh) using below command:
+
+```
+chmod +x aerialist_setup.sh
+./aerialist_setup.sh
+```
+
+
+## Step-by-Step Installation
+
+If you can not use the above script, these are step by step guides to install PX4 and its dependencies.
+
+1. [install python >= 3.8](https://www.itsupportwale.com/blog/how-to-upgrade-to-python-3-8-on-ubuntu-18-04-lts/) (if not already installed)
+
+- [if terminal doesn't show up anymore](https://askubuntu.com/questions/1132349/terminal-not-opening-up-after-upgrading-python-to-3-7) (use VS code terminal)
+
+2. [setup PX4 development toolchain](https://docs.px4.io/main/en/dev_setup/dev_env_linux_ubuntu.html#ros-melodic-ubuntu-18-04)
+
+3. [clone and run PX4-Autopilot](https://docs.px4.io/main/en/dev_setup/building_px4.html) (skip the steps already done)
+
+- checkout version v1.12.3: `git checkout tags/v1.12.3 --recursive-modules`
+- fixing potential errors you may encounter:
+  - [Build error GSTREAMER](https://github.com/PX4/PX4-Autopilot/issues/13117)
+  - [jMAVsim fails with Java 11](https://github.com/PX4/jMAVSim/issues/96#issuecomment-500788800) and then [this](https://github.com/PX4/PX4-Autopilot/issues/9557#issuecomment-512137607) and [this](https://github.com/PX4/PX4-Autopilot/issues/9557#issuecomment-589559521)
+
+5. [install QGC](https://docs.qgroundcontrol.com/master/en/getting_started/download_and_install.html#ubuntu)
+
+- on ubuntu 18, check [this](https://github.com/mavlink/qgroundcontrol/issues/9847#issuecomment-918133080) if you can not run
+
+6. [setup PX4-Avoidance](https://github.com/PX4/PX4-Avoidance#installation) (start from point 7)
+
+7. check if everything works
\ No newline at end of file
diff --git a/docs/architecture.md b/docs/architecture.md
new file mode 100644
index 0000000..6512bdb
--- /dev/null
+++ b/docs/architecture.md
@@ -0,0 +1,39 @@
+# Software Architecture
+
+Below figure demonstrates the overall architecture of Aerialist and it is described in mored details in the [later sections](#software-architecture).
+The implementation currently supports [PX4-Autopilot](https://github.com/PX4/PX4-Autopilot), but can be extended to support other UAV platforms as well.
+
+![Aerialist Architecture](architecture.png)
+
+To evaluate a test definition, we generate and execute the corresponding simulated test case automatically. The test case automates all necessary steps: setting up the test environment, building/running the firmware code, running/configuring the simulator with the simulated world properties, connecting the simulated UAV to the firmware, and applying the UAV configurations from the test case properties at startup.
+Then, the test case commands are scheduled and sent to the UAV, the flight is monitored for any issues, and after test completion, the flight log file is extracted.
+
+## Test Description
+
+The de-facto testing standard of UAVs relies on manually-written system-level tests to test UAVs in the field. These tests are defined as specific software configurations (using parameters, config files, etc.), in a specific environment setup (e.g., obstacles placement, lightning conditions), and a set of runtime commands. Such runtime commands received during the UAV flight (from RC, GCS, onboard computers, etc.), make the UAV fly with a specific human observable behavior (e.g., flight trajectory, speed, distance to obstacles). We model a UAV test case as a set of test properties (e.g., ⟨configuration, environment, commands⟩) that control the flight and a set of pre-defined UAV expected states (e.g., ⟨trajectory positions⟩) during the flight. More specifically we define a test case by the following properties, which are fed to Aerialist as an input file, or command arguments, and refer to them as test description:
+
+- Configs: Drone configuration at startup (all parameter values and configuration files required to start the simulation).
+- Commands: Timestamped external commands from the ground station or the remote controller to the drone during the flight (e.g., change flight mode, go in a specific direction, enter mission mode).
+- Environment (optional): Simulated world’s configurations (e.g., used simulator, obstacles’ position and shape, wind speed and direction).
+- Expectation (optional): time series of certain sensor reading that the test flights are expected to follow closely.
+
+## Generator
+
+The Generator module deals with setting up the simulated world before testing UAVs in SIL mode.
+It sets up and prepares the simulation environment as described in the test description, in a specific simulator (e.g., Gazebo, jMAVSim), along with the described static and dynamic objects and simulated UAV.
+Configurator. module is responsible for setting up and initialising the UAV under test (either simulated or real) before flying the UAV, according to the  instructions in the test description. This includes building the code, connecting to the drone via MAVLink, setting the parameters, uploading any needed resources, etc.
+
+## Commander
+
+The Commander module is responsible for all the runtime communications to the UAV, including scheduling and sending the Remote Control (RC) commands (e.g., manual sticks, flight mode changes, arm/disarm), communications from Ground Controll Station (GCS) or the offboard commands coming from a companion computer.
+
+## Monitor
+
+The Monitor module is the module responsible for runtime analysis of UAV state during the flight. Using MAVLink, we are able to subscribe to any messages communicated between PX4 modules, including sensor values. These messages allow monitoring any runtime checks described in the test description to evaluate monitoring functionalities before deploying on the UAV. The tested and finalised monitoring solutions can then be developed
+directly in the PX4 firmware as an on board module, or as a ROS module running on the companion computer.
+
+## Analyst
+
+The Analyst module is responsible for any post-flight analysis, mostly based on the extracted flight log. It parses the ULog
+files, and extracts any important and relevant data to analyse test result based on the given expectations in the
+test description.
diff --git a/docs/architecture.png b/docs/architecture.png
old mode 100644
new mode 100755
index 3917b1b..e37006f
Binary files a/docs/architecture.png and b/docs/architecture.png differ
diff --git a/docs/k8s_setup.md b/docs/k8s_setup.md
new file mode 100644
index 0000000..4a2aa65
--- /dev/null
+++ b/docs/k8s_setup.md
@@ -0,0 +1,54 @@
+# Kubernetes Test Execution
+
+Aerialist can also deploy test executions on a Kubernetes cluster to facilitate running tests in the cloud. Specifically, as can be seen in the below figure, Aerialist can run multiple executions of the same test case in isolated Kubernets pods in parallel, and gather test results for further processing.
+
+This feature is specifically helpful when performing test generation tasks, where UAV's flight could be subject to non-determinism and multiple simulations are required.
+
+<p align="center"><img src="deployment.png" alt="Kubernetes Deployment" width="60%"/></p>
+<!-- ![Kubernetes Deployment](docs/deployment.png) -->
+
+Aerialist can connect both to a cloud Kubernetes cluster, or a local instance (more useful during development).
+
+- Requirements:
+  - [kubectl](https://kubernetes.io/docs/tasks/tools/#kubectl)
+    - [Set default context and namespace](https://kubernetes.io/docs/reference/kubectl/cheatsheet/#kubectl-context-and-configuration) to your prefered clster and namespace if needed
+  - [yq](https://github.com/mikefarah/yq#install)
+
+      ```bash
+      wget https://github.com/mikefarah/yq/releases/download/v4.22.1/yq_linux_amd64 -O /usr/bin/yq &&\
+          chmod +x /usr/bin/yq
+      ```
+
+## Using Local Kubernetes Instance
+
+This guide explains how to set up a local Kubernetes instance using Docker Desktop for the purpose of running a specific task.
+
+1. **Install Docker Desktop**: Download and install [Docker Desktop](https://www.docker.com/products/docker-desktop/) on your Ubuntu 20.04 machine.
+
+2. **Enable Local Kubernetes**: In Docker Desktop, enable local Kubernetes. You can find this option in the Docker Desktop settings.
+
+3. **Generate a Kubeconfig File**: Open a terminal and run the following command to generate a kubeconfig file for your local Kubernetes instance:
+   `kubectl config view --raw --minify > k8s-config.yaml`
+
+3. **Upload Kubeconfig as a ConfigMap**: Create a Kubernetes ConfigMap named k8s-config by uploading the kubeconfig file generated:
+   `kubectl config view --raw --minify > k8s-config.yaml`
+
+
+5. **Set Environment variable**: Set the environment variable `KUBE_USE_VOLUME` to `True` in your environment.
+
+6. **Specify Output Folder**: Edit the path in the `k8s-job-avoidance-local.yaml` or `k8s-job-local.yaml` file to specify the desired output folder within your project.
+
+7. **Run the Task**: `python3 aerialist exec --test samples/tests/mission1-k8s.yaml`
+
+## Using Cloud Kubernetes Cluster
+
+Aerialist uses a [NextCloud](https://nextcloud.com/) instance to share files between the main container, and the parallel test executers. You can get a free account in [a cloud provider](https://nextcloud.com/sign-up/) or deploy your own [dockerized instance](https://hub.docker.com/_/nextcloud).
+
+1. Set your NextCloud credentials and address in as a k8s-Secret:
+  `kubectl create secret generic webdav --from-literal=host=https://[your-nextcloud-address]/remote.php/dav/files/[your-account-id]/ --from-literal=user=[username] --from-literal=pass=[password]`
+
+2. Upload your [`k8s-config.yaml`](https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/) as a k8s-ConfigMap:
+  `kubectl create configmap k8s-config --from-file k8s-config.yaml`
+
+3. You can now use `--agent k8s` in the commands to run the simulations in your k8s-cluster.
+  `python3 aerialist exec --test samples/tests/mission1.yaml --agent k8s -n 5 --id mission-test --path webdav://`
diff --git a/requirements.txt b/requirements.txt
index c802929..5c17ec1 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -15,4 +15,5 @@ pyyaml==6.0
 munch==2.5.0
 rospkg==1.4.0
 defusedxml==0.7.1
-tslearn== 0.5.2
\ No newline at end of file
+tslearn==0.5.2
+h5py==3.9.0
\ No newline at end of file
diff --git a/samples/snippets/README.md b/samples/snippets/README.md
new file mode 100644
index 0000000..17a8d5d
--- /dev/null
+++ b/samples/snippets/README.md
@@ -0,0 +1,10 @@
+# Using Aerialist in Your Code
+
+You can integrate Aerialist's python package in your own code and directly define and execute UAV test cases with it.
+This can be speccifically useful when you are working on test generation approaches for UAVs. An example of such usage of Aerialist can be found in [Surrealist](https://github.com/skhatiri/Surrealist).
+
+1. `pip3 install git+https://github.com/skhatiri/Aerialist.git`
+2. Create an instance of [DroneTest](./aerialist/px4/drone_test.py) class and define your test case
+3. configure and execute the test case using your prefered test execution agent ([LocalAgent](./aerialist/px4/local_agent.py), [DockerAgent](./aerialist/px4/docker_agent.py), [K8sAgent](./aerialist/px4/k8s_agent.py))
+
+- Take a look at our [code snippets](./samples/snippets/) for more details and sample codes.
diff --git a/setup.py b/setup.py
index a891042..57305cc 100644
--- a/setup.py
+++ b/setup.py
@@ -5,7 +5,7 @@
 
 setuptools.setup(
     name="aerialist",
-    version="0.1.16",
+    version="0.2.01",
     author="Sajad Khatiri",
     author_email="s.khatiri@gmail.com",
     description="UAV Test Bench",
@@ -36,6 +36,7 @@
         "rospkg==1.4.0",
         "defusedxml==0.7.1",
         "tslearn== 0.5.2",
+        "h5py==3.9.0",
     ],
     entry_points={"console_scripts": ["aerialist=aerialist.entry:main"]},
 )
diff --git a/setup_script/aerialist_setup.sh b/setup_script/aerialist_setup.sh
new file mode 100644
index 0000000..7037869
--- /dev/null
+++ b/setup_script/aerialist_setup.sh
@@ -0,0 +1,83 @@
+#!/bin/bash
+
+cd ~
+git clone https://github.com/skhatiri/Aerialist.git
+
+
+
+
+source ~/anaconda3/bin/activate Aerialist
+cd ~/Aerialist
+pip3 install -r ~/Aerialist/requirements.txt
+pip3 install kconfiglib
+pip3 install empy
+pip3 install future
+pip3 install packaging
+pip3 install toml
+pip3 install numpy
+pip3 install jinja2
+pip3 install cython
+pip3 install pyyaml
+pip3 install future
+pip3 install python-decouple
+pip3 install --user pyros-genmsg
+pip3 install --user jsonschema
+
+cd ~
+PWD=$(pwd)
+echo $PWD
+CATKIN_HOME=$PWD"/catkin_ws/"
+PX4_HOME=$PWD"/Firmware/"
+ROS_HOME=$PWD"/.ros/"
+echo $PX4_HOME
+cd ~/Aerialist
+cp template.env .env
+SEARCH_STRING="PX4_HOME=/src/PX4-Autopilot/"
+REPLACE_STRING="PX4_HOME="$PX4_HOME
+sed -i "s|$SEARCH_STRING|$REPLACE_STRING|g" .env
+SEARCH_STRING="CATKIN_HOME=/src/catkin_ws/"
+REPLACE_STRING="CATKIN_HOME="$CATKIN_HOME
+sed -i "s|$SEARCH_STRING|$REPLACE_STRING|g" .env
+SEARCH_STRING="ROS_HOME=/root/.ros/"
+REPLACE_STRING="ROS_HOME="$ROS_HOME
+sed -i "s|$SEARCH_STRING|$REPLACE_STRING|g" .env
+SEARCH_STRING="SIMULATOR=gazebo"
+REPLACE_STRING="SIMULATOR=ros"
+sed -i "s|$SEARCH_STRING|$REPLACE_STRING|g" .env
+SEARCH_STRING="DRONE=sitl"
+REPLACE_STRING="DRONE=ros"
+sed -i "s|$SEARCH_STRING|$REPLACE_STRING|g" .env
+sed -i 'export ROS_PACKAGE_PATH=${ROS_PACKAGE_PATH}:~/Firmware' "~/.bashrc"
+
+
+make -C /home/aerialist/Firmware px4_sitl_default gazebo &
+
+# Capture the process ID (PID) of the make command
+make_pid=$!
+
+# Wait for the build to complete (you can adjust the timeout as needed)
+# You can also check the build process output for specific completion messages
+wait $make_pid
+
+# Check if the make command is still running
+if ps -p $make_pid > /dev/null; then
+    echo "Terminating the build process..."
+    # Terminate the make command
+    kill $make_pid
+else
+    echo "Build completed successfully."
+fi
+
+# Continue with other commands in your script
+# ...
+
+# Optionally, you can use an exit code to indicate success or failure
+# Exit with 0 if the build completed successfully, or a different code if it failed
+if ps -p $make_pid > /dev/null; then
+    exit 1  # Build failed
+else
+    exit 0  # Build succeeded
+fi
+
+
+exit 0
diff --git a/setup_script/px4_avoidance_aerialist_setup.sh b/setup_script/px4_avoidance_aerialist_setup.sh
new file mode 100644
index 0000000..eff4e50
--- /dev/null
+++ b/setup_script/px4_avoidance_aerialist_setup.sh
@@ -0,0 +1,4 @@
+#!/bin/bash
+
+source px4_avoidance_setup.sh
+source aerialist_setup.sh
\ No newline at end of file
diff --git a/setup_script/px4_avoidance_setup.sh b/setup_script/px4_avoidance_setup.sh
new file mode 100644
index 0000000..4095779
--- /dev/null
+++ b/setup_script/px4_avoidance_setup.sh
@@ -0,0 +1,60 @@
+#!/bin/bash
+
+sudo apt install curl
+mkdir ~/tmp
+cd ~/tmp
+curl -O https://repo.anaconda.com/archive/Anaconda3-2023.07-2-Linux-x86_64.sh
+bash Anaconda3-2023.07-2-Linux-x86_64.sh -b -u
+source ~/anaconda3/bin/activate
+conda init bash
+conda create --name Aerialist python=3.9 -y
+source ~/anaconda3/bin/activate Aerialist
+pip3 install future
+pip3 install kconfiglib
+pip3 install empy
+pip3 install future
+pip3 install packaging
+pip3 install toml
+pip3 install numpy
+pip3 install jinja2
+pip3 install cython
+pip3 install pyyaml
+pip3 install future
+pip3 install --user pyros-genmsg
+pip3 install --user jsonschema
+cd ~
+wget https://raw.githubusercontent.com/PX4/Devguide/master/build_scripts/ubuntu_sim_ros_melodic.sh
+
+source ~/anaconda3/bin/activate Aerialist
+cd ~
+source ubuntu_sim_ros_melodic.sh
+sudo apt-get install ros-melodic-geographic-msgs -y
+cd ~
+source ubuntu_sim_ros_melodic.sh
+
+source ~/anaconda3/bin/activate Aerialist
+
+
+sudo apt install openjdk-8-jdk
+sudo update-alternatives --config java -2
+file_path="/etc/java-8-openjdk/accessibility.properties"
+sed -i '$s/^/#/' "$file_path"
+sudo apt install ros-melodic-mavros ros-melodic-mavros-extras -y
+sudo apt install libpcl1 ros-melodic-octomap-* -y
+sudo apt-get install libgstreamer-plugins-base1.0-dev -y
+
+cd ~
+git clone https://github.com/PX4/Firmware.git --recursive
+cd ~/Firmware
+
+git fetch origin release/1.13
+git checkout release/1.13
+make submodulesclean
+
+cd ~/catkin_ws/src
+git clone https://github.com/PX4/avoidance.git
+
+catkin build -w ~/catkin_ws
+
+echo "source ~/catkin_ws/devel/setup.bash" >> ~/.bashrc
+source ~/.bashrc
diff --git a/template.env b/template.env
index d63bbe8..deb4b86 100644
--- a/template.env
+++ b/template.env
@@ -43,6 +43,5 @@ DOCKER_TIMEOUT=1000
 # WEBDAV_DL_FLD=/tmp/
 # WEBDAV_UP_FLD=tmp/
 # WEBDAV_HOST=
-# WEBDAV_ROOT=
 # WEBDAV_USER=
 # WEBDAV_PASS=


Test patch:
diff --git a/aerialist/px4/drone_test.py b/aerialist/px4/drone_test.py
index 035a580..515df9a 100644
--- a/aerialist/px4/drone_test.py
+++ b/aerialist/px4/drone_test.py
@@ -26,6 +26,8 @@ def __init__(
         if simulation is not None and simulation.home_position is None:
             if assertion is not None and assertion.log_file is not None:
                 simulation.home_position = Trajectory.get_home(assertion.log_file)
+        if simulation is not None and test is not None:
+            self.test.speed = self.simulation.speed
 
     @classmethod
     def from_yaml(cls, address):
@@ -49,9 +51,23 @@ def from_yaml(cls, address):
 
     def to_yaml(self, address):
         with open(address, "w") as file:
-            yaml.dump(self, file)
+            yaml.dump(self.to_dict(), file)
         return address
 
+    def to_dict(self):
+        dic = {}
+        if self.drone is not None:
+            dic["drone"] = self.drone.to_dict()
+        if self.simulation is not None:
+            dic["simulation"] = self.simulation.to_dict()
+        if self.test is not None:
+            dic["test"] = self.test.to_dict()
+        if self.agent is not None:
+            dic["agent"] = self.agent.to_dict()
+        if self.assertion is not None:
+            dic["assertion"] = self.assertion.to_dict()
+        return dic
+
     def cmd_params(self):
         # CMD must be updated if the interface in entry.py changes
         params = "exec "
@@ -106,6 +122,27 @@ def cmd_params(self):
 
         return params
 
+    @classmethod
+    def plot(
+        cls, test: DroneTest, results: List[DroneTestResult], obstacle_distance=True
+    ) -> None:
+        distance = None
+        if obstacle_distance:
+            distance = True
+        elif test.assertion is not None and test.assertion.expectation is not None:
+            distance = median(
+                [r.record.distance(test.assertion.expectation) for r in results]
+            )
+        if results is not None and len(results) >= 1:
+            Trajectory.plot_multiple(
+                [r.record for r in results],
+                goal=None if test.assertion is None else test.assertion.expectation,
+                distance=distance,
+                obstacles=None
+                if test.simulation is None
+                else test.simulation.obstacles,
+            )
+
 
 class DroneConfig:
     CF_PORT = 14550
@@ -141,6 +178,18 @@ def __init__(
         if mission_file is not None:
             self.mission_file = file_helper.get_local_file(mission_file)
 
+    def to_dict(self):
+        dic = {}
+        if self.port is not None:
+            dic["port"] = self.port
+        if self.params is not None:
+            dic["params"] = self.params
+        elif self.params_file is not None:
+            dic["params_file"] = self.params_file
+        if self.mission_file is not None:
+            dic["mission_file"] = self.mission_file
+        return dic
+
 
 class SimulationConfig:
     JMAVSIM = "jmavsim"
@@ -167,7 +216,25 @@ def __init__(
             and len(obstacles) > 0
             and not isinstance(obstacles[0], Obstacle)
         ):
-            self.obstacles = Obstacle.from_coordinates_multiple(obstacles)
+            if isinstance(obstacles[0], munch.DefaultMunch):
+                self.obstacles = Obstacle.from_dict_multiple(obstacles)
+            else:
+                self.obstacles = Obstacle.from_coordinates_multiple(obstacles)
+
+    def to_dict(self):
+        dic = {}
+        if self.simulator is not None:
+            dic["simulator"] = self.simulator
+        if self.world != "default":
+            dic["world"] = self.world
+        if self.speed != 1:
+            dic["speed"] = self.speed
+        dic["headless"] = self.headless
+        if self.obstacles is not None:
+            dic["obstacles"] = [obs.to_dict() for obs in self.obstacles]
+        if self.home_position is not None:
+            dic["home_position"] = self.home_position
+        return dic
 
 
 class TestConfig:
@@ -180,9 +247,21 @@ def __init__(
         self.speed = speed
         self.commands = commands
         self.commands_file = commands_file
+        if commands is not None and type(commands[0]) != Command:
+            self.commands = [Command(**c) for c in commands]
         if commands is None and commands_file is not None:
             self.commands = Command.extract(file_helper.get_local_file(commands_file))
 
+    def to_dict(self):
+        dic = {}
+        if self.commands is not None and len(self.commands) < 10:
+            dic["commands"] = [c.to_dict() for c in self.commands]
+        elif self.commands_file is not None:
+            dic["commands_file"] = self.commands_file
+        if self.speed != 1:
+            dic["speed"] = self.speed
+        return dic
+
 
 class AssertionConfig:
     TRAJECTORY = "trajectory"
@@ -203,6 +282,14 @@ def __init__(
                     file_helper.get_local_file(log_file)
                 )
 
+    def to_dict(self):
+        dic = {}
+        if self.log_file is not None:
+            dic["log_file"] = self.log_file
+        if self.variable is not None:
+            dic["variable"] = self.variable
+        return dic
+
 
 class AgentConfig:
     K8S = "k8s"
@@ -221,6 +308,17 @@ def __init__(
         self.path = path
         self.id = id
 
+    def to_dict(self):
+        dic = {}
+        dic["engine"] = self.engine
+        if self.count != 1:
+            dic["count"] = self.count
+        if self.path is not None:
+            dic["path"] = self.path
+        if self.id is not None:
+            dic["id"] = self.id
+        return dic
+
 
 class DroneTestResult:
     def __init__(
@@ -245,17 +343,3 @@ def load_folder(
         logs = file_helper.get_logs_address(logs_folder)
         results = [DroneTestResult(log, variable) for log in logs]
         return results
-
-
-def Plot(test: DroneTest, results: List[DroneTestResult]) -> None:
-    if results is not None and len(results) >= 1:
-        Trajectory.plot_multiple(
-            [r.record for r in results],
-            goal=None if test.assertion is None else test.assertion.expectation,
-            distance=None
-            if test.assertion is None or test.assertion.expectation is None
-            else median(
-                [r.record.distance(test.assertion.expectation) for r in results]
-            ),
-            obstacles=None if test.simulation is None else test.simulation.obstacles,
-        )
diff --git a/aerialist/px4/test_agent.py b/aerialist/px4/test_agent.py
index 237718d..a27e64e 100644
--- a/aerialist/px4/test_agent.py
+++ b/aerialist/px4/test_agent.py
@@ -7,5 +7,5 @@ def __init__(self, config: DroneTest) -> None:
         self.config = config
         self.results: List[DroneTestResult] = []
 
-    def Run(self, config: DroneTest) -> DroneTestResult:
+    def run(self) -> DroneTestResult:
         pass
diff --git a/docs/test_plot.png b/docs/test_plot.png
new file mode 100644
index 0000000..32bdea0
Binary files /dev/null and b/docs/test_plot.png differ
diff --git a/samples/snippets/test_execution.py b/samples/snippets/test_execution.py
new file mode 100644
index 0000000..1c4efb8
--- /dev/null
+++ b/samples/snippets/test_execution.py
@@ -0,0 +1,31 @@
+from aerialist.px4.drone_test import DroneTest
+from aerialist.px4.docker_agent import DockerAgent
+from aerialist.px4.obstacle import Obstacle
+from aerialist.px4.trajectory import Trajectory
+
+
+### load a yaml test file
+test = DroneTest.from_yaml("samples/tests/mission1-local.yaml")
+
+### manipulate test properties
+### just a few samples here
+# test.simulation.obstacles = [Obstacle()]
+# test.agent.engine=test.agent.DOCKER
+# test.drone.mission_file = new_mission_file_address
+#
+
+### execute the test using the docker agent
+agent = DockerAgent(test)
+test_results = agent.run()
+
+### plot the test
+DroneTest.plot(test, test_results)
+
+### get the test flight trajectory
+trajectory = test_results[0].record
+
+### plot the trajecotory
+trajectory.plot()
+
+### get minimum drone distance to the obstacles
+distance = trajectory.distance_to_obstacles(test.simulation.obstacles)
diff --git a/samples/tests/manual1-docker.yaml b/samples/tests/manual1-docker.yaml
deleted file mode 100644
index 3ce97e2..0000000
--- a/samples/tests/manual1-docker.yaml
+++ /dev/null
@@ -1,20 +0,0 @@
-drone:
-  port: sitl #{sitl, ros, cf}
-  
-simulation:
-  simulator: gazebo #{gazebo,jmavsim,ros} 
-  speed: 1
-  headless: false
-
-test:
-  commands_file: samples/flights/manual1-commands.csv
-  speed: 1
-
-assertion:
-  log_file: samples/flights/manual1.ulg
-  # variable: trajectory
-
-agent:
-  engine: docker # {k8s, docker, local}
-  count: 1
-
diff --git a/samples/tests/manual1-replay-k8s.yaml b/samples/tests/manual1-k8s.yaml
similarity index 81%
rename from samples/tests/manual1-replay-k8s.yaml
rename to samples/tests/manual1-k8s.yaml
index 3d43234..8a29695 100644
--- a/samples/tests/manual1-replay-k8s.yaml
+++ b/samples/tests/manual1-k8s.yaml
@@ -8,7 +8,6 @@ simulation:
 
 test:
   commands_file: samples/flights/manual1.ulg
-  speed: 1
 
 assertion:
   log_file: samples/flights/manual1.ulg
@@ -17,5 +16,5 @@ assertion:
 agent:
   engine: k8s # {k8s, docker, local}
   count: 5
-  path: https://filer.cloudlab.zhaw.ch/remote.php/webdav/test/
+  path: webdav://test/ # /home/git/Aerialist/results
   id: yaml-test
\ No newline at end of file
diff --git a/samples/tests/manual1-local.yaml b/samples/tests/manual1.yaml
similarity index 56%
rename from samples/tests/manual1-local.yaml
rename to samples/tests/manual1.yaml
index ef4b4c0..0c14b3e 100644
--- a/samples/tests/manual1-local.yaml
+++ b/samples/tests/manual1.yaml
@@ -4,17 +4,11 @@ drone:
 simulation:
   simulator: gazebo #{gazebo,jmavsim,ros} 
   speed: 1
-  headless: false
+  headless: true
 
 test:
-  commands_file: samples/flights/manual1-commands.csv
-  speed: 1
+  commands_file: samples/flights/manual1.ulg
 
 assertion:
   log_file: samples/flights/manual1.ulg
   variable: trajectory
-
-agent:
-  engine: local # {k8s, docker, local}
-  count: 1
-
diff --git a/samples/tests/mission1-k8s.yaml b/samples/tests/mission1-k8s.yaml
index fc5365d..05a3af4 100644
--- a/samples/tests/mission1-k8s.yaml
+++ b/samples/tests/mission1-k8s.yaml
@@ -7,11 +7,19 @@ simulation:
   simulator: ros #{gazebo, jmavsim, ros} 
   speed: 1
   headless: true
-  obstacles: [5,5,5,  -8,3,0,  0] #propetries of box shaped obstacles, [l,w,h,x,y,z,r]
+  obstacles:
+  - size:
+      l: 5
+      w: 5
+      h: 5
+    position:
+      x: -8
+      y: 3
+      z: 0
+      r: 0
   # home_position: [lat,lon,alt]  
 test:
   commands_file: samples/flights/mission1-commands.csv
-  speed: 1
 
 assertion:
   log_file: samples/flights/mission1.ulg
@@ -20,6 +28,6 @@ assertion:
 agent:
   engine: k8s # {k8s, docker, local}
   count: 5
-  path: https://filer.cloudlab.zhaw.ch/remote.php/webdav/test/
+  path: webdav://test/ # /home/git/Aerialist/results 
   id: yaml-test
 
diff --git a/samples/tests/mission1-local.yaml b/samples/tests/mission1-local.yaml
deleted file mode 100644
index bb6f14e..0000000
--- a/samples/tests/mission1-local.yaml
+++ /dev/null
@@ -1,23 +0,0 @@
-drone:
-  port: ros #{sitl, ros, cf}
-  params_file: samples/flights/mission1-params.csv 
-  mission_file: samples/flights/mission1.plan
-
-simulation:
-  simulator: ros #{gazebo, jmavsim, ros} 
-  speed: 1
-  headless: false
-  obstacles: [5,5,5,  -8,3,0,  0] # size,position,orientation of a box
-
-test:
-  commands_file: samples/flights/mission1-commands.csv
-  speed: 1
-
-assertion:
-  log_file: samples/flights/mission1.ulg
-  variable: trajectory
-
-agent:
-  engine: local # {k8s, docker, local}
-  count: 1
-
diff --git a/samples/tests/mission1-docker.yaml b/samples/tests/mission1.yaml
similarity index 66%
rename from samples/tests/mission1-docker.yaml
rename to samples/tests/mission1.yaml
index 2c71131..6b70d1b 100644
--- a/samples/tests/mission1-docker.yaml
+++ b/samples/tests/mission1.yaml
@@ -7,17 +7,24 @@ simulation:
   simulator: ros #{gazebo, jmavsim, ros} 
   speed: 1
   headless: true
-  obstacles: [5,5,5,  -8,3,0,  0] # size,position,orientation of a box
+  obstacles:
+  - size:
+      l: 5
+      w: 5
+      h: 5
+    position:
+      x: -8
+      y: 3
+      z: 0
+      r: 0
 
 test:
   commands_file: samples/flights/mission1-commands.csv
-  speed: 1
 
 assertion:
   log_file: samples/flights/mission1.ulg
   variable: trajectory
 
-agent:
-  engine: docker # {k8s, docker, local}
-  count: 1
-
+# agent:
+#   engine: local # {k8s, docker, local}
+#   count: 1
diff --git a/samples/tests/template-test.yaml b/samples/tests/template-test.yaml
index 9ead59b..4b9cddb 100644
--- a/samples/tests/template-test.yaml
+++ b/samples/tests/template-test.yaml
@@ -15,7 +15,6 @@ simulation:
   # home_position: [lat,lon,alt]  
 test:
   commands_file: samples/flights/mission1-commands.csv
-  speed: 1
 
 assertion:
   log_file: samples/flights/mission1.ulg
@@ -24,6 +23,6 @@ assertion:
 agent:
   engine: k8s # {k8s, docker, local}
   count: 5
-  path: https://filer.cloudlab.zhaw.ch/remote.php/webdav/test/
+  path: webdav://test/
   id: yaml-test
 
diff --git a/samples/tests/template.yaml b/samples/tests/template.yaml
index 4f66306..d92c1b5 100644
--- a/samples/tests/template.yaml
+++ b/samples/tests/template.yaml
@@ -20,5 +20,5 @@ assertion:
 agent:
   engine: k8s # test execution environment {k8s, docker, local}
   count: 5 # no. of parallel runs (only for k8s)
-  path: https://path/to/cloud/storage/ # shared cloud storage (only for k8s)
+  path: webdav://path/to/cloud/storage/ # shared cloud storage (only for k8s)
   id: k8s-id # k8s job id (only for k8s)
\ No newline at end of file
diff --git a/tests/test_local_agent.py b/tests/test_local_agent.py
index 978599e..9a05a08 100644
--- a/tests/test_local_agent.py
+++ b/tests/test_local_agent.py
@@ -1,7 +1,6 @@
 import unittest
 import os
 import logging
-from aerialist.px4.command import Command, DefaultCommands
 from aerialist.px4.drone import Drone
 from aerialist.px4.drone_test import (
     AssertionConfig,