diff --git a/.gitignore b/.gitignore
index 9d824fe64a4c864975d4b78b493ca11df2fcb1a9..043b66a95a35968ee555bd349ade5dddc7a2dc0c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
 **/__pycache__
+build
 dist
 *.egg
 *.egg-info*
@@ -8,6 +9,7 @@ dist
 report.xml
 htmlcov
 docs
+env
 !tests/resources/*.hdf5
 
 *.ipynb_checkpoints
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index e9300a2487b01778102c5d10a11ba8053384113c..d579b9a074657960670aa4eedd0dcf8c2bc73fed 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,23 +1,23 @@
 stages:
-  - test
   - package
+  - test
   - deploy
 
 .centos:
   tags: [linux, docker]
   image: git.imp.fu-berlin.de:5000/bioroboticslab/auto/ci/centos:latest
 
+.macos:
+  tags: [macos, shell]
+
 .windows:
   tags: [windows, docker]
   image: git.imp.fu-berlin.de:5000/bioroboticslab/auto/ci/windows:latest-devel
   before_script:
     - . $Profile.AllUsersAllHosts
 
-.cpy37: &cpy37
-  PYTHON_EXECUTABLE: "python3.7"
-
-.cpy38: &cpy38
-  PYTHON_EXECUTABLE: "python3.8"
+.python38: &python38
+  PYTHON_VERSION: "3.8"
 
 .test: &test
   stage: test
@@ -28,79 +28,53 @@ stages:
   script:
     - ./ci/test.py
 
-test centos[cpy37]:
-  extends: .centos
-  <<: *test
-  variables:
-    <<: [*cpy37]
-
-test centos[cpy38]:
-  extends: .centos
-  <<: *test
-  variables:
-    <<: [*cpy38]
-
-.test windows[cpy37]:
-  extends: .windows
-  <<: *test
-
 .package: &package
   stage: package
   artifacts:
     paths:
       - dist
     expire_in: 1 week
+    reports:
+      dotenv: build.env
   script:
     - ./ci/package.py
 
-package centos[cpy37]:
+package:
   extends: .centos
-  dependencies:
-    - test centos[cpy37]
   <<: *package
   variables:
-    <<: [*cpy37]
+    <<: [*python38]
 
-package centos[cpy38]:
+"test: [centos, 3.8]":
   extends: .centos
-  dependencies:
-    - test centos[cpy38]
-  <<: *package
-  variables:
-    <<: [*cpy38]
+  <<: *test
 
-.package windows[cpy37]:
-  extends: .windows
-  dependencies:
-    - test windows[cpy37]
-  <<: *package
+"test: [macos, 3.8]":
+  extends: .macos
+  <<: *test
 
-deploy centos[cpy37]:
-  extends: .centos
-  stage: deploy
-  only:
-    - tags
-  dependencies:
-    - package centos[cpy37]
-  script:
-    - ./ci/deploy.py
+"test: [windows, 3.8]":
+  extends: .windows
+  <<: *test
 
-.deploy centos[cpy38]:
+deploy to staging:
   extends: .centos
   stage: deploy
   only:
+    - master
     - tags
+  allow_failure: true
   dependencies:
-    - package centos[cpy38]
+    - package
   script:
     - ./ci/deploy.py
 
-.deploy windows[cpy37]:
+deploy to production:
   extends: .centos
   stage: deploy
   only:
     - tags
   dependencies:
-    - package windows[cpy37]
+    - package
   script:
-    - ./ci/deploy.py
+    - ./ci/deploy.py --production
diff --git a/README.md b/README.md
index 3e195a064a107c23890aed7667a7d1cde879105d..5da734e05530add0c7ef723c9990f3634dbcd57f 100644
--- a/README.md
+++ b/README.md
@@ -16,12 +16,12 @@ This repository implements an easy to use interface, to create, save, load, and
 
 Quick variant:
 ```
-pip install robofish-trackviewer robofish-io --extra-index-url https://git.imp.fu-berlin.de/api/v4/projects/6392/packages/pypi/simple
+pip3 install robofish-trackviewer robofish-io --extra-index-url https://git.imp.fu-berlin.de/api/v4/projects/6392/packages/pypi/simple
 ```
 
 Better variant:
 - Follow instructions at [Artifacts repository](https://git.imp.fu-berlin.de/bioroboticslab/robofish/artifacts)
-- ```pip install robofish-io```
+- ```pip3 install robofish-io```
 
 
 ## Usage
@@ -31,14 +31,14 @@ We show a simple example below. More examples can be found in ```examples/```
 import robofish.io
 import numpy as np
 
-filename = "example.hdf5"
 
+# Create a new robofish io file
 f = robofish.io.File(world_size_cm=[100, 100], frequency_hz=25.0)
 f.attrs["experiment_setup"] = "This is a simple example with made up data."
 
-# Create a single robot with 30 timesteps
-# positions are passed separately
-# orientations are passed as with two columns -> orientation_x and orientation_y
+# Create a new robot entity. Positions and orientations are passed
+# separately in this example. Since the orientations have two columns,
+# unit vectors are assumed (orientation_x, orientation_y)
 f.create_entity(
     category="robot",
     name="robot",
@@ -46,7 +46,9 @@ f.create_entity(
     orientations=np.ones((100, 2)) * [0, 1],
 )
 
-# Create fishes with 30 poses (x, y, orientation_rad)
+# Create a new fish entity.
+# In this case, we pass positions and orientations together (x, y, rad).
+# Since it is a 3 column array, orientations in radiants are assumed.
 poses = np.zeros((100, 3))
 poses[:, 0] = np.arange(-50, 50)
 poses[:, 1] = np.arange(-50, 50)
@@ -57,12 +59,9 @@ fish.attrs["fish_standard_length_cm"] = 10
 
 # Show and save the file
 print(f)
-print("Poses Shape: ", f.get_poses().shape)
-
-# Saving also validates the file
-f.save(filename)
-print(f"Saved to {filename}")
+print("Poses Shape: ", f.entity_poses.shape)
 
+f.save_as(path)
 ```
 
 ### Evaluation
@@ -72,7 +71,7 @@ Current modes are:
 - turn
 - tank_positions
 - trajectories
-- follow_ii
+- follow_iid
 
 
 ## LICENSE
diff --git a/ci/deploy.py b/ci/deploy.py
index 6dda82b345943f9e96b35acb1d1858343e51a1c8..0437457696a996d508cb3b4235ba6feaeb49d52c 100755
--- a/ci/deploy.py
+++ b/ci/deploy.py
@@ -3,22 +3,31 @@
 
 from os import environ as env
 from subprocess import check_call
-from pathlib import Path
 from platform import system
+from argparse import ArgumentParser
 
 
 if __name__ == "__main__":
     if system() != "Linux":
         raise Exception("Uploading python package only supported on Linux")
 
+    p = ArgumentParser()
+    p.add_argument("--production", default=False, action="store_const", const=True)
+    args = p.parse_args()
+
     env["TWINE_USERNAME"] = "gitlab-ci-token"
     env["TWINE_PASSWORD"] = env["CI_JOB_TOKEN"]
 
+    if args.production:
+        target_project_id = env['ARTIFACTS_REPOSITORY_PROJECT_ID']
+    else:
+        target_project_id = env['CI_PROJECT_ID']
+
     command = ["python3"]
     command += ["-m", "twine", "upload", "dist/*"]
     command += [
         "--repository-url",
-        f"https://git.imp.fu-berlin.de/api/v4/projects/{env['ARTIFACTS_REPOSITORY_PROJECT_ID']}/packages/pypi",
+        f"https://git.imp.fu-berlin.de/api/v4/projects/{target_project_id}/packages/pypi",
     ]
 
     check_call(command)
diff --git a/ci/package.py b/ci/package.py
index 4be105e3f11445e3c9c8e9d183882fcac116d8ba..38ef2d7f540eb37068ae0a0df5123173962e2789 100755
--- a/ci/package.py
+++ b/ci/package.py
@@ -2,16 +2,17 @@
 # SPDX-License-Identifier: LGPL-3.0-or-later
 
 from os import environ as env
-from platform import system
 from subprocess import check_call
-from sys import executable
+from pathlib import Path
+from platform import system
+from shutil import which
 
 
 def python_executable():
     if system() == "Windows":
-        return executable
-    elif system() == "Linux":
-        return env["PYTHON_EXECUTABLE"]
+        return f"/Python{''.join(env['PYTHON_VERSION'].split('.'))}/python.exe"
+    elif system() == "Linux" or system() == "Darwin":
+        return which(f"python{env['PYTHON_VERSION']}")
     assert False
 
 
@@ -20,3 +21,6 @@ if __name__ == "__main__":
     command += ["setup.py", "bdist_wheel"]
 
     check_call(command)
+
+    with open("build.env", "w") as f:
+        f.write(f"PYTHON_VERSION={env['PYTHON_VERSION']}\n")
diff --git a/ci/test.py b/ci/test.py
index 992a7cf573a716ff839a26989fd45dab6ce66c6c..ab41980d6b6bae18e8bf990f162b20b57edd82af 100755
--- a/ci/test.py
+++ b/ci/test.py
@@ -1,28 +1,50 @@
 #! /usr/bin/env python3
 # SPDX-License-Identifier: LGPL-3.0-or-later
 
-from os import environ as env
+from os import environ as env, pathsep
 from subprocess import check_call
+from pathlib import Path
 from platform import system
-from sys import executable
+from shutil import which
 
 
 def python_executable():
     if system() == "Windows":
-        return executable
-    elif system() == "Linux":
-        return env["PYTHON_EXECUTABLE"]
+        return f"/Python{''.join(env['PYTHON_VERSION'].split('.'))}/python.exe"
+    elif system() == "Linux" or system() == "Darwin":
+        return which(f"python{env['PYTHON_VERSION']}")
+    assert False
+
+
+def python_venv_executable():
+    if system() == "Windows":
+        return str(Path(".venv/Scripts/python.exe").resolve())
+    elif system() == "Linux" or system() == "Darwin":
+        return str(Path(".venv/bin/python").resolve())
     assert False
 
 
 if __name__ == "__main__":
-    check_call([python_executable(), "-m", "pip", "install", "pytest"])
-    # check_call([python_executable(), "-m", "pip", "install", "pytest-cov"])
-    check_call([python_executable(), "-m", "pip", "install", "h5py"])
-    check_call([python_executable(), "-m", "pip", "install", "pandas"])
-    check_call([python_executable(), "-m", "pip", "install", "deprecation"])
+    check_call(
+        [
+            python_executable(),
+            "-m",
+            "venv",
+            "--system-site-packages",
+            "--prompt",
+            "ci",
+            ".venv",
+        ]
+    )
 
-    command = [python_executable()]
-    command += ["-m", "pytest", "--junitxml=report.xml"]
+    check_call(
+        [
+            python_venv_executable(),
+            "-m",
+            "pip",
+            "install",
+            str(sorted(Path("dist").glob("*.whl"))[-1].resolve()),
+        ]
+    )
 
-    check_call(command)
+    check_call([python_venv_executable(), "-m", "pytest", "--junitxml=report.xml"])
diff --git a/examples/__init__.py b/examples/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/examples/example_basic.ipynb b/examples/example_basic.ipynb
index 253ff2fceacac9ad10c6399edf19a2211ed55b9e..eba81c56c7c497751bffce4c944b4ae3212dac15 100644
--- a/examples/example_basic.ipynb
+++ b/examples/example_basic.ipynb
@@ -2,7 +2,65 @@
  "cells": [
   {
    "cell_type": "code",
-   "execution_count": 1,
+   "execution_count": 20,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import robofish.io\n",
+    "import numpy as np\n",
+    "\n",
+    "\n",
+    "def create_example_file(path):\n",
+    "    # Create a new io file object with a 100x100cm world\n",
+    "    f = robofish.io.File(world_size_cm=[100, 100], frequency_hz=25.0)\n",
+    "\n",
+    "    # create a simple obstacle, fixed in place, fixed outline\n",
+    "    obstacle_outline = [[[-10, -10], [-10, 0], [0, 0], [0, -10]]]\n",
+    "    obstacle_name = f.create_entity(\n",
+    "        \"obstacle\", positions=[[50, 50]], orientations=[[0]], outlines=obstacle_outline\n",
+    "    )\n",
+    "\n",
+    "    # create a robofish with 1000 timesteps. If we would not give a name, the name would be generated to be robot_1.\n",
+    "    robofish_timesteps = 1000\n",
+    "    robofish_poses = np.tile([50, 50, 1, 0], (robofish_timesteps, 1))\n",
+    "    robot = f.create_entity(\"robot\", name=\"robot\", poses=robofish_poses)\n",
+    "\n",
+    "    # create multiple fishes with timestamps. Since we don't specify names, but only the type \"fish\" the fishes will be named [\"fish_1\", \"fish_2\", \"fish_3\"]\n",
+    "    agents = 3\n",
+    "    timesteps = 1000\n",
+    "    # timestamps = np.linspace(0, timesteps + 1, timesteps)\n",
+    "    agent_poses = np.random.random((agents, timesteps, 3))\n",
+    "\n",
+    "    fishes = f.create_multiple_entities(\"fish\", agent_poses)\n",
+    "\n",
+    "    # This would throw an exception if the file was invalid\n",
+    "    f.validate()\n",
+    "\n",
+    "    # Save file validates aswell\n",
+    "    f.save_as(path)\n",
+    "\n",
+    "    # Closing and opening files (just for demonstration). When opening with r+, we can read and write afterwards.\n",
+    "    f.close()\n",
+    "    f = robofish.io.File(path, \"r+\")\n",
+    "\n",
+    "    print(\"\\nEntity Names\")\n",
+    "    print(f.entity_names)\n",
+    "\n",
+    "    # Get an array with all poses. As the length of poses varies per agent, it is filled up with nans.\n",
+    "    print(\"\\nAll poses\")\n",
+    "    print(f.entity_poses)\n",
+    "\n",
+    "    # Select all entities with the category fish\n",
+    "    print(\"\\nFish poses\")\n",
+    "    print(f.select_entity_poses(lambda e: e.category == \"fish\"))\n",
+    "\n",
+    "    print(\"\\nFinal file\")\n",
+    "    print(f)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 21,
    "metadata": {},
    "outputs": [
     {
@@ -14,156 +72,116 @@
       "['fish_1', 'fish_2', 'fish_3', 'obstacle_1', 'robot']\n",
       "\n",
       "All poses\n",
-      "[[[8.86584103e-01 2.35670820e-01 5.41754842e-01 4.49850202e-01]\n",
-      "  [8.15511882e-01 4.78223324e-01 6.29803419e-01 1.12592392e-01]\n",
-      "  [1.53732300e-01 7.24954247e-01 9.38574493e-01 4.65665817e-01]\n",
-      "  [9.10354614e-01 4.47880208e-01 3.81429136e-01 9.67544317e-01]\n",
-      "  [6.07822955e-01 5.20158827e-01 8.17965686e-01 8.42760384e-01]]\n",
+      "[[[5.20669639e-01 4.58831601e-02 9.51813161e-01 3.06678474e-01]\n",
+      "  [4.54089224e-01 4.62401301e-01 7.62699127e-01 6.46753490e-01]\n",
+      "  [9.67400968e-01 5.63468635e-01 9.99894381e-01 1.45336846e-02]\n",
+      "  ...\n",
+      "  [8.80278572e-02 3.80126536e-01 9.97331321e-01 7.30081499e-02]\n",
+      "  [3.28493923e-01 6.17436647e-01 9.20374036e-01 3.91039193e-01]\n",
+      "  [7.53752515e-02 2.74342328e-01 7.84065127e-01 6.20678544e-01]]\n",
       "\n",
-      " [[2.29353935e-01 8.80753636e-01 7.94585168e-01 2.22074524e-01]\n",
-      "  [6.13970399e-01 1.33511815e-02 2.89155185e-01 2.65219092e-01]\n",
-      "  [6.62197351e-01 6.47982001e-01 9.46004018e-02 6.59599364e-01]\n",
-      "  [4.86104101e-01 4.23153102e-01 1.39821902e-01 3.11809748e-01]\n",
-      "  [8.03322852e-01 9.52799857e-01 3.89638603e-01 6.43237352e-01]]\n",
+      " [[4.45818096e-01 5.39128423e-01 5.66779137e-01 8.23869765e-01]\n",
+      "  [4.38290328e-01 3.78231764e-01 9.88384008e-01 1.51977092e-01]\n",
+      "  [1.82629541e-01 8.08078349e-01 8.27510357e-01 5.61450422e-01]\n",
+      "  ...\n",
+      "  [5.48624545e-02 9.00771558e-01 8.00554574e-01 5.99259913e-01]\n",
+      "  [8.34412515e-01 2.47002933e-02 8.98045361e-01 4.39902902e-01]\n",
+      "  [2.35550269e-01 5.17610013e-01 7.89557755e-01 6.13676250e-01]]\n",
       "\n",
-      " [[9.70978260e-01 6.75936878e-01 6.23196602e-01 8.42264950e-01]\n",
-      "  [4.07079160e-01 8.46290290e-01 5.64092159e-01 3.56871307e-01]\n",
-      "  [4.84096229e-01 8.60232174e-01 1.39015794e-01 7.82253265e-01]\n",
-      "  [1.24170482e-01 2.21511930e-01 8.88282284e-02 4.53450561e-01]\n",
-      "  [1.28404438e-01 2.87771430e-02 4.57022637e-01 9.80571806e-01]]\n",
+      " [[4.05882373e-02 9.17375386e-01 9.94780242e-01 1.02040365e-01]\n",
+      "  [8.21412623e-01 6.68225110e-01 9.35957074e-01 3.52114081e-01]\n",
+      "  [8.17060947e-01 2.38414750e-01 9.85274673e-01 1.70979008e-01]\n",
+      "  ...\n",
+      "  [1.20162942e-01 4.16668624e-01 9.17142212e-01 3.98560166e-01]\n",
+      "  [9.19857025e-01 7.85686851e-01 6.58272266e-01 7.52779961e-01]\n",
+      "  [8.69306684e-01 5.62790215e-01 9.19795334e-01 3.92398477e-01]]\n",
       "\n",
-      " [[5.00000000e+01 5.00000000e+01 0.00000000e+00 0.00000000e+00]\n",
+      " [[5.00000000e+01 5.00000000e+01 1.00000000e+00 0.00000000e+00]\n",
       "  [           nan            nan            nan            nan]\n",
       "  [           nan            nan            nan            nan]\n",
+      "  ...\n",
+      "  [           nan            nan            nan            nan]\n",
       "  [           nan            nan            nan            nan]\n",
       "  [           nan            nan            nan            nan]]\n",
       "\n",
-      " [[0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00]\n",
-      "  [0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00]\n",
-      "  [0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00]\n",
-      "  [0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00]\n",
-      "  [           nan            nan            nan            nan]]]\n",
+      " [[5.00000000e+01 5.00000000e+01 1.00000000e+00 0.00000000e+00]\n",
+      "  [5.00000000e+01 5.00000000e+01 1.00000000e+00 0.00000000e+00]\n",
+      "  [5.00000000e+01 5.00000000e+01 1.00000000e+00 0.00000000e+00]\n",
+      "  ...\n",
+      "  [5.00000000e+01 5.00000000e+01 1.00000000e+00 0.00000000e+00]\n",
+      "  [5.00000000e+01 5.00000000e+01 1.00000000e+00 0.00000000e+00]\n",
+      "  [5.00000000e+01 5.00000000e+01 1.00000000e+00 0.00000000e+00]]]\n",
       "\n",
       "Fish poses\n",
-      "[[[0.8865841  0.23567082 0.54175484 0.4498502 ]\n",
-      "  [0.81551188 0.47822332 0.62980342 0.11259239]\n",
-      "  [0.1537323  0.72495425 0.93857449 0.46566582]\n",
-      "  [0.91035461 0.44788021 0.38142914 0.96754432]\n",
-      "  [0.60782295 0.52015883 0.81796569 0.84276038]]\n",
+      "[[[0.52066964 0.04588316 0.95181316 0.30667847]\n",
+      "  [0.45408922 0.4624013  0.76269913 0.64675349]\n",
+      "  [0.96740097 0.56346864 0.99989438 0.01453368]\n",
+      "  ...\n",
+      "  [0.08802786 0.38012654 0.99733132 0.07300815]\n",
+      "  [0.32849392 0.61743665 0.92037404 0.39103919]\n",
+      "  [0.07537525 0.27434233 0.78406513 0.62067854]]\n",
       "\n",
-      " [[0.22935393 0.88075364 0.79458517 0.22207452]\n",
-      "  [0.6139704  0.01335118 0.28915519 0.26521909]\n",
-      "  [0.66219735 0.647982   0.0946004  0.65959936]\n",
-      "  [0.4861041  0.4231531  0.1398219  0.31180975]\n",
-      "  [0.80332285 0.95279986 0.3896386  0.64323735]]\n",
+      " [[0.4458181  0.53912842 0.56677914 0.82386976]\n",
+      "  [0.43829033 0.37823176 0.98838401 0.15197709]\n",
+      "  [0.18262954 0.80807835 0.82751036 0.56145042]\n",
+      "  ...\n",
+      "  [0.05486245 0.90077156 0.80055457 0.59925991]\n",
+      "  [0.83441252 0.02470029 0.89804536 0.4399029 ]\n",
+      "  [0.23555027 0.51761001 0.78955775 0.61367625]]\n",
       "\n",
-      " [[0.97097826 0.67593688 0.6231966  0.84226495]\n",
-      "  [0.40707916 0.84629029 0.56409216 0.35687131]\n",
-      "  [0.48409623 0.86023217 0.13901579 0.78225327]\n",
-      "  [0.12417048 0.22151193 0.08882823 0.45345056]\n",
-      "  [0.12840444 0.02877714 0.45702264 0.98057181]]]\n",
+      " [[0.04058824 0.91737539 0.99478024 0.10204037]\n",
+      "  [0.82141262 0.66822511 0.93595707 0.35211408]\n",
+      "  [0.81706095 0.23841475 0.98527467 0.17097901]\n",
+      "  ...\n",
+      "  [0.12016294 0.41666862 0.91714221 0.39856017]\n",
+      "  [0.91985703 0.78568685 0.65827227 0.75277996]\n",
+      "  [0.86930668 0.56279022 0.91979533 0.39239848]]]\n",
       "\n",
       "File structure\n",
-      " version:\t[1 0]\n",
-      " world size:\t[100. 100.]\n",
+      " format_url:\thttps://git.imp.fu-berlin.de/bioroboticslab/robofish/track_format/-/releases/1.0\n",
+      " format_version:\t[1 0]\n",
+      " world_size_cm:\t[100. 100.]\n",
       "| entities\n",
       "|---| fish_1\n",
-      "|---|--- type:\tfish\n",
-      "|---|--- poses:\t Shape (5, 4)\n",
-      "|---|---| time\n",
-      "|---|---|--- monotonic points:\t Shape (5,)\n",
+      "|---|--- category:\tfish\n",
+      "|---|--- orientations:\t Shape (1000, 2)\n",
+      "|---|--- positions:\t Shape (1000, 2)\n",
       "|---| fish_2\n",
-      "|---|--- type:\tfish\n",
-      "|---|--- poses:\t Shape (5, 4)\n",
-      "|---|---| time\n",
-      "|---|---|--- monotonic points:\t Shape (5,)\n",
+      "|---|--- category:\tfish\n",
+      "|---|--- orientations:\t Shape (1000, 2)\n",
+      "|---|--- positions:\t Shape (1000, 2)\n",
       "|---| fish_3\n",
-      "|---|--- type:\tfish\n",
-      "|---|--- poses:\t Shape (5, 4)\n",
-      "|---|---| time\n",
-      "|---|---|--- monotonic points:\t Shape (5,)\n",
+      "|---|--- category:\tfish\n",
+      "|---|--- orientations:\t Shape (1000, 2)\n",
+      "|---|--- positions:\t Shape (1000, 2)\n",
       "|---| obstacle_1\n",
-      "|---|--- type:\tobstacle\n",
+      "|---|--- category:\tobstacle\n",
+      "|---|--- orientations:\t Shape (1, 2)\n",
       "|---|--- outlines:\t Shape (1, 4, 2)\n",
-      "|---|--- poses:\t Shape (1, 4)\n",
-      "|---|---| time\n",
+      "|---|--- positions:\t Shape (1, 2)\n",
       "|---| robot\n",
-      "|---|--- type:\trobot\n",
-      "|---|--- poses:\t Shape (4, 4)\n",
-      "|---|---| time\n",
-      "|---|---|--- monotonic step:\t40\n",
+      "|---|--- category:\trobot\n",
+      "|---|--- orientations:\t Shape (1000, 2)\n",
+      "|---|--- positions:\t Shape (1000, 2)\n",
+      "| samplings\n",
+      "|--- default:\t25 hz\n",
+      "|---| 25 hz\n",
+      "|---|--- frequency_hz:\t25.0\n",
       "\n"
      ]
     }
    ],
    "source": [
-    "#! /usr/bin/env python3\n",
-    "\n",
-    "import robofish.io\n",
-    "import numpy as np\n",
-    "from pathlib import Path\n",
-    "import os\n",
-    "\n",
-    "\n",
-    "# Helper function to enable relative paths from this file\n",
-    "def full_path(path):\n",
-    "    return (Path(os.path.abspath(\"__file__\")).parent / path).resolve()\n",
-    "\n",
-    "\n",
     "if __name__ == \"__main__\":\n",
-    "    # Create a new io file object with a 100x100cm world\n",
-    "    sf = robofish.io.File(world_size=[100, 100])\n",
-    "\n",
-    "    # create a simple obstacle, fixed in place, fixed outline\n",
-    "    obstacle_pose = [[50, 50, 0, 0]]\n",
-    "    obstacle_outline = [[[-10, -10], [-10, 0], [0, 0], [0, -10]]]\n",
-    "    obstacle_name = sf.create_entity(\n",
-    "        \"obstacle\", poses=obstacle_pose, outlines=obstacle_outline\n",
-    "    )\n",
-    "\n",
-    "    # create a robofish with 100 timesteps and 40ms between the timesteps. If we would not give a name, the name would be generated to be robot_1.\n",
-    "    robofish_timesteps = 4\n",
-    "    robofish_poses = np.zeros((robofish_timesteps, 4))\n",
-    "    sf.create_entity(\"robot\", robofish_poses, name=\"robot\", monotonic_step=40)\n",
-    "\n",
-    "    # create multiple fishes with timestamps. Since we don't specify names, but only the type \"fish\" the fishes will be named [\"fish_1\", \"fish_2\", \"fish_3\"]\n",
-    "    agents = 3\n",
-    "    timesteps = 5\n",
-    "    timestamps = np.linspace(0, timesteps + 1, timesteps)\n",
-    "    agent_poses = np.random.random((agents, timesteps, 4))\n",
-    "\n",
-    "    fish_names = sf.create_multiple_entities(\n",
-    "        \"fish\", agent_poses, monotonic_points=timestamps\n",
-    "    )\n",
-    "\n",
-    "    # This would throw an exception if the file was invalid\n",
-    "    sf.validate()\n",
-    "\n",
-    "    # Save file validates aswell\n",
-    "    example_file = full_path(\"example.hdf5\")\n",
-    "    sf.save(example_file)\n",
-    "\n",
-    "    # Closing and opening files (just for demonstration)\n",
-    "    sf.close()\n",
-    "    sf = robofish.io.File(path=example_file)\n",
-    "\n",
-    "    print(\"\\nEntity Names\")\n",
-    "    print(sf.get_entity_names())\n",
-    "\n",
-    "    # Get an array with all poses. As the length of poses varies per agent, it\n",
-    "    # is filled up with nans. The result is not interpolated and the time scales\n",
-    "    # per agent are different. It is planned to create a warning in the case of\n",
-    "    # different time scales and have another function, which generates an\n",
-    "    # interpolated array.\n",
-    "    print(\"\\nAll poses\")\n",
-    "    print(sf.get_poses_array())\n",
-    "\n",
-    "    print(\"\\nFish poses\")\n",
-    "    print(sf.get_poses_array(fish_names))\n",
-    "\n",
-    "    print(\"\\nFile structure\")\n",
-    "    print(sf)\n"
+    "    create_example_file(\"example.hdf5\")"
    ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": []
   }
  ],
  "metadata": {
diff --git a/examples/example_basic.py b/examples/example_basic.py
index e328d380144d327512f7476e3493e687b57b43d8..34e092296f6edde6301a13623c15f77a46ddb70a 100755
--- a/examples/example_basic.py
+++ b/examples/example_basic.py
@@ -1,59 +1,54 @@
-#! /usr/bin/env python3
-
 import robofish.io
 import numpy as np
-from pathlib import Path
-import os
-
 
-# Helper function to enable relative paths from this file
-def full_path(path):
-    return (Path(os.path.abspath("__file__")).parent / path).resolve()
 
-
-if __name__ == "__main__":
+def create_example_file(path):
     # Create a new io file object with a 100x100cm world
-    sf = robofish.io.File(world_size_cm=[100, 100], frequency_hz=25.0)
+    f = robofish.io.File(world_size_cm=[100, 100], frequency_hz=25.0)
 
     # create a simple obstacle, fixed in place, fixed outline
     obstacle_outline = [[[-10, -10], [-10, 0], [0, 0], [0, -10]]]
-    obstacle_name = sf.create_entity(
+    obstacle_name = f.create_entity(
         "obstacle", positions=[[50, 50]], orientations=[[0]], outlines=obstacle_outline
     )
 
-    # create a robofish with 100 timesteps and 40ms between the timesteps. If we would not give a name, the name would be generated to be robot_1.
+    # create a robofish with 1000 timesteps. If we would not give a name, the name would be generated to be robot_1.
     robofish_timesteps = 1000
-    robofish_poses = np.ones((robofish_timesteps, 4)) * 50
-    robot = sf.create_entity("robot", robofish_poses, name="robot")
+    robofish_poses = np.tile([50, 50, 1, 0], (robofish_timesteps, 1))
+    robot = f.create_entity("robot", name="robot", poses=robofish_poses)
 
     # create multiple fishes with timestamps. Since we don't specify names, but only the type "fish" the fishes will be named ["fish_1", "fish_2", "fish_3"]
     agents = 3
     timesteps = 1000
     # timestamps = np.linspace(0, timesteps + 1, timesteps)
-    agent_poses = np.random.random((agents, timesteps, 4))
+    agent_poses = np.random.random((agents, timesteps, 3))
 
-    fishes = sf.create_multiple_entities("fish", agent_poses)
+    fishes = f.create_multiple_entities("fish", agent_poses)
 
     # This would throw an exception if the file was invalid
-    sf.validate()
+    f.validate()
 
     # Save file validates aswell
-    example_file = full_path("example.hdf5")
-    sf.save(example_file)
+    f.save_as(path)
 
-    # Closing and opening files (just for demonstration)
-    sf.close()
-    sf = robofish.io.File(path=example_file)
+    # Closing and opening files (just for demonstration). When opening with r+, we can read and write afterwards.
+    f.close()
+    f = robofish.io.File(path, "r+")
 
     print("\nEntity Names")
-    print(sf.get_entity_names())
+    print(f.entity_names)
 
     # Get an array with all poses. As the length of poses varies per agent, it is filled up with nans.
     print("\nAll poses")
-    print(sf.get_poses())
+    print(f.entity_poses)
 
+    # Select all entities with the category fish
     print("\nFish poses")
-    print(sf.get_poses(category="fish"))
+    print(f.select_entity_poses(lambda e: e.category == "fish"))
 
-    print("\nFile structure")
-    print(sf)
+    print("\nFinal file")
+    print(f)
+
+
+if __name__ == "__main__":
+    create_example_file("example.hdf5")
diff --git a/examples/example_readme.py b/examples/example_readme.py
index 0ec1c8deb0279722bff22f79a982592af2526a98..62df2336f89281a0d9306428839e9aee2921ae4b 100644
--- a/examples/example_readme.py
+++ b/examples/example_readme.py
@@ -1,34 +1,39 @@
 import robofish.io
 import numpy as np
 
-filename = "example.hdf5"
-
-f = robofish.io.File(world_size_cm=[100, 100], frequency_hz=25.0)
-f.attrs["experiment_setup"] = "This is a simple example with made up data."
-
-# Create a single robot with 30 timesteps
-# positions are passed separately
-# orientations are passed as with two columns -> orientation_x and orientation_y
-f.create_entity(
-    category="robot",
-    name="robot",
-    positions=np.zeros((100, 2)),
-    orientations=np.ones((100, 2)) * [0, 1],
-)
-
-# Create fishes with 30 poses (x, y, orientation_rad)
-poses = np.zeros((100, 3))
-poses[:, 0] = np.arange(-50, 50)
-poses[:, 1] = np.arange(-50, 50)
-poses[:, 2] = np.arange(0, 2 * np.pi, step=2 * np.pi / 100)
-fish = f.create_entity("fish", poses=poses)
-fish.attrs["species"] = "My rotating spaghetti fish"
-fish.attrs["fish_standard_length_cm"] = 10
-
-# Show and save the file
-print(f)
-print("Poses Shape: ", f.get_poses().shape)
-
-# Saving also validates the file
-f.save(filename)
-print(f"Saved to {filename}")
+
+def create_example_file(path):
+    # Create a new robofish io file
+    f = robofish.io.File(world_size_cm=[100, 100], frequency_hz=25.0)
+    f.attrs["experiment_setup"] = "This is a simple example with made up data."
+
+    # Create a new robot entity. Positions and orientations are passed
+    # separately in this example. Since the orientations have two columns,
+    # unit vectors are assumed (orientation_x, orientation_y)
+    f.create_entity(
+        category="robot",
+        name="robot",
+        positions=np.zeros((100, 2)),
+        orientations=np.ones((100, 2)) * [0, 1],
+    )
+
+    # Create a new fish entity.
+    # In this case, we pass positions and orientations together (x, y, rad).
+    # Since it is a 3 column array, orientations in radiants are assumed.
+    poses = np.zeros((100, 3))
+    poses[:, 0] = np.arange(-50, 50)
+    poses[:, 1] = np.arange(-50, 50)
+    poses[:, 2] = np.arange(0, 2 * np.pi, step=2 * np.pi / 100)
+    fish = f.create_entity("fish", poses=poses)
+    fish.attrs["species"] = "My rotating spaghetti fish"
+    fish.attrs["fish_standard_length_cm"] = 10
+
+    # Show and save the file
+    print(f)
+    print("Poses Shape: ", f.entity_poses.shape)
+
+    f.save_as(path)
+
+
+if __name__ == "__main__":
+    path = create_example_file("example.hdf5")
diff --git a/setup.py b/setup.py
index ce7fa01d0447d8634eda4aa8e6a8ad47b685f6c5..552f54e96c461e588672a3238c82b98cee1b8030 100644
--- a/setup.py
+++ b/setup.py
@@ -1,5 +1,7 @@
 # SPDX-License-Identifier: LGPL-3.0-or-later
 
+from subprocess import run, PIPE
+
 from setuptools import setup, find_packages
 
 
@@ -11,12 +13,42 @@ entry_points = {
         "robofish-io-evaluate=robofish.evaluate.app:evaluate",
     ]
 }
+
+
+def source_version():
+    version_parts = (
+        run(
+            ["git", "describe", "--tags", "--dirty"],
+            check=True,
+            stdout=PIPE,
+            encoding="utf-8",
+        )
+        .stdout.strip()
+        .split("-")
+    )
+
+    if version_parts[-1] == "dirty":
+        dirty = True
+        version_parts = version_parts[:-1]
+    else:
+        dirty = False
+
+    version = version_parts[0]
+    if len(version_parts) == 3:
+        version += ".post0"
+        version += f".dev{version_parts[1]}+{version_parts[2]}"
+    if dirty:
+        version += "+dirty"
+
+    return version
+
+
 setup(
     name="robofish-io",
-    version="0.1",
+    version=source_version(),
     author="",
     author_email="",
-    install_requires=["h5py>=3", "numpy", "seaborn", "pandas", "deprecation"],
+    install_requires=["h5py>=2.10.0", "numpy", "seaborn", "pandas", "deprecation"],
     classifiers=[
         "Development Status :: 3 - Alpha",
         "Intended Audience :: Science/Research",
@@ -27,7 +59,7 @@ setup(
         "Programming Language :: Python :: 3.8",
     ],
     python_requires=">=3.6",
-    packages=find_packages("src"),
+    packages=[f"robofish.{p}" for p in find_packages("src/robofish")],
     package_dir={"": "src"},
     zip_safe=True,
     entry_points=entry_points,
diff --git a/src/conversion_scripts/convert_marc.py b/src/conversion_scripts/convert_marc.py
index bf68ef3b115baa54f29ec020d0b68635df678b5b..ae130f2a4a9d6e63ebc4daec0733164c70d1b1c7 100644
--- a/src/conversion_scripts/convert_marc.py
+++ b/src/conversion_scripts/convert_marc.py
@@ -6,7 +6,7 @@ This script converts csv files created by Marc to IO files.
 The script is not maintained, since Marc changed his code to generate io files.
 """
 
-# Jan 2021 Andreas Gerken, Berlin, Germany
+# Jan 2021 Marc Groeling, Berlin, Germany
 # Released under GNU 3.0 License
 # email andi.gerken@gmail.com
 
@@ -38,4 +38,4 @@ def convertTrajectory(path, save_path, categories):
     )
 
     new.validate()
-    new.save(save_path)
\ No newline at end of file
+    new.save(save_path)
diff --git a/src/robofish/evaluate/evaluate.py b/src/robofish/evaluate/evaluate.py
index 3ab73916aa1f91dcda906595ed01c1707c16d5cb..d47e0d56472b36076009340f03bdd31087f85dca 100644
--- a/src/robofish/evaluate/evaluate.py
+++ b/src/robofish/evaluate/evaluate.py
@@ -19,8 +19,13 @@ from typing import Iterable
 from scipy import stats
 
 
+<<<<<<< HEAD
 # def get_all_poses_from_paths(paths: Iterable(str)):
 #     """This function reads all poses from given paths.
+=======
+def get_all_poses_from_paths(paths: Iterable[str]):
+    """This function reads all poses from given paths.
+>>>>>>> master
 
 #     Args:
 #         paths: An array of strings, with files or folders
@@ -30,8 +35,13 @@ from scipy import stats
 #     # Open all files, shape (paths, files)
 #     files_per_path = [robofish.io.read_multiple_files(p) for p in paths]
 
+<<<<<<< HEAD
 #     # Read all poses from the files, shape (paths, files)
 #     poses_per_path = [[f.get_poses() for f in files] for files in files_per_path]
+=======
+    # Read all poses from the files, shape (paths, files)
+    poses_per_path = [[f.entity_poses for f in files] for files in files_per_path]
+>>>>>>> master
 
 #     # close all files
 #     for p in files_per_path:
@@ -49,6 +59,7 @@ def evaluate_speed(
     for k, files in enumerate(files_per_path):
         path_speeds = []
         for p, file in files.items():
+            # TODO: Change to select_poses()
             poses = file.get_poses(
                 names=None if consider_names is None else consider_names[k],
                 category=None
@@ -57,7 +68,7 @@ def evaluate_speed(
             )
             for e_poses in poses:
                 e_speeds = np.linalg.norm(np.diff(e_poses[:, :2], axis=0), axis=1)
-                e_speeds *= file.get_frequency()
+                e_speeds *= file.frequency
                 path_speeds.extend(e_speeds)
         speeds.append(path_speeds)
 
@@ -87,6 +98,7 @@ def evaluate_turn(
     for k, files in enumerate(files_per_path):
         path_turns = []
         for p, file in files.items():
+            # TODO: Change to select_poses()
             poses = file.get_poses(
                 names=None if consider_names is None else consider_names[k],
                 category=None
@@ -95,7 +107,7 @@ def evaluate_turn(
             )
 
             # Todo check if all frequencies are the same
-            frequency = file.get_frequency()
+            frequency = file.frequency
 
             for e_poses in poses:
                 # convert ori_x, ori_y to radians
@@ -104,7 +116,7 @@ def evaluate_turn(
                 e_turns = ori_rad[1:] - ori_rad[:-1]
                 e_turns = np.where(e_turns < -np.pi, e_turns + 2 * np.pi, e_turns)
                 e_turns = np.where(e_turns > np.pi, e_turns - 2 * np.pi, e_turns)
-                # e_turns *= file.get_frequency()
+                # e_turns *= file.frequency
                 e_turns *= 180 / np.pi
                 path_turns.extend(e_turns)
         turns.append(path_turns)
@@ -135,6 +147,7 @@ def evaluate_orientation(
     orientations = []
     for k, files in enumerate(files_per_path):
         for p, file in files.items():
+            # TODO: Change to select_poses()
             poses = file.get_poses(
                 names=None if consider_names is None else consider_names[k],
                 category=None
@@ -206,13 +219,14 @@ def evaluate_relativeOrientation(
     for k, files in enumerate(files_per_path):
         path_orientations = []
         for p, file in files.items():
+            # TODO: Change to select_poses()
             poses = file.get_poses(
                 names=None if consider_names is None else consider_names[k],
                 category=None
                 if consider_categories is None
                 else consider_categories[k],
             )
-            all_poses = file.get_poses()
+            all_poses = file.entity_poses
             for i in range(len(poses)):
                 for j in range(len(all_poses)):
                     if (poses[i] != all_poses[j]).any():
@@ -255,6 +269,7 @@ def evaluate_distanceToWall(
         for p, file in files.items():
             worldBoundsX.append(file.attrs["world_size_cm"][0])
             worldBoundsY.append(file.attrs["world_size_cm"][1])
+            # TODO: Change to select_poses()
             poses = file.get_poses(
                 names=None if consider_names is None else consider_names[k],
                 category=None
@@ -326,6 +341,7 @@ def evaluate_tankpositions(
     for k, files in enumerate(files_per_path):
         path_x_pos, path_y_pos = [], []
         for p, file in files.items():
+            # TODO: Change to select_poses()
             poses = file.get_poses(
                 names=None if consider_names is None else consider_names[k],
                 category=None
@@ -371,6 +387,7 @@ def evaluate_trajectories(
     world_bounds = []
     for k, files in enumerate(files_per_path):
         for p, file in files.items():
+            # TODO: Change to select_poses()
             poses = file.get_poses(
                 names=None if consider_names is None else consider_names[k],
                 category=None
@@ -445,13 +462,14 @@ def evaluate_positionVec(
         for p, file in files.items():
             worldBoundsX.append(file.attrs["world_size_cm"][0])
             worldBoundsY.append(file.attrs["world_size_cm"][1])
+            # TODO: Change to select_poses()
             poses = file.get_poses(
                 names=None if consider_names is None else consider_names[k],
                 category=None
                 if consider_categories is None
                 else consider_categories[k],
             )
-            all_poses = file.get_poses()
+            all_poses = file.entity_poses
             # calculate posVec for every fish combination
             for i in range(len(poses)):
                 for j in range(len(all_poses)):
@@ -461,8 +479,7 @@ def evaluate_positionVec(
                         )
                         path_posVec.append(
                             calculate_posVec(
-                                posVec_input,
-                                np.arctan2(poses[i, :, 3], poses[i, :, 2]),
+                                posVec_input, np.arctan2(poses[i, :, 3], poses[i, :, 2])
                             )
                         )
         posVec.append(np.concatenate(path_posVec, axis=0))
@@ -506,13 +523,14 @@ def evaluate_follow_iid(
         for p, file in files.items():
             worldBoundsX.append(file.attrs["world_size_cm"][0])
             worldBoundsY.append(file.attrs["world_size_cm"][1])
+            # TODO: Change to select_poses()
             poses = file.get_poses(
                 names=None if consider_names is None else consider_names[k],
                 category=None
                 if consider_categories is None
                 else consider_categories[k],
             )
-            all_poses = file.get_poses()
+            all_poses = file.entity_poses
             for i in range(len(poses)):
                 for j in range(len(all_poses)):
                     if (poses[i] != all_poses[j]).any():
diff --git a/src/robofish/io/__init__.py b/src/robofish/io/__init__.py
index 7801db3eba7dcbc1979b5e1b77542906fa79285a..5e84ac01da5eccb45e7879027dc2589f851895e7 100644
--- a/src/robofish/io/__init__.py
+++ b/src/robofish/io/__init__.py
@@ -7,6 +7,7 @@ from robofish.io.file import *
 from robofish.io.entity import *
 from robofish.io.validation import *
 from robofish.io.io import *
+from robofish.io.utils import *
 import robofish.io.app
 
 
diff --git a/src/robofish/io/entity.py b/src/robofish/io/entity.py
index e5329988e71b9f8e54018aee9e339b614ad73f64..669ac1746ab15dd5c24e6eae1b6986bf97c6a49b 100644
--- a/src/robofish/io/entity.py
+++ b/src/robofish/io/entity.py
@@ -1,4 +1,6 @@
 import robofish.io
+import robofish.io.utils as utils
+
 import h5py
 import numpy as np
 from typing import Iterable, Union
@@ -24,6 +26,16 @@ class Entity(h5py.Group):
         outlines: Iterable = None,
         sampling: str = None,
     ):
+        poses, positions, orientations, outlines = utils.np_array(
+            poses, positions, orientations, outlines
+        )
+
+        assert poses is None or (poses.ndim == 2 and poses.shape[1] in [3, 4])
+        assert positions is None or (positions.ndim == 2 and positions.shape[1] == 2)
+        assert orientations is None or (
+            orientations.ndim == 2 and orientations.shape[1] in [1, 2]
+        )
+
         # If no name is given, create one from type and an id
         if name is None:
             i = 1
@@ -47,15 +59,28 @@ class Entity(h5py.Group):
 
     @classmethod
     def convert_rad_to_vector(cla, orientations_rad):
-        ori_rad = np.array(orientations_rad)
+        if min(orientations_rad) < 0 or max(orientations_rad) > 2 * np.pi:
+            logging.warning(
+                "Converting orientations, from a bigger range than [0, 2 * pi]. When passing the orientations, they are assumed to be in radiants."
+            )
+        ori_rad = utils.np_array(orientations_rad)
         assert ori_rad.shape[1] == 1
         ori_vec = np.empty((ori_rad.shape[0], 2))
         ori_vec[:, 0] = np.cos(ori_rad[:, 0])
         ori_vec[:, 1] = np.sin(ori_rad[:, 0])
         return ori_vec
 
-    def getName(self):
-        return self.name.split("/")[-1]
+    @property
+    def group_name(self):
+        return super().name
+
+    @property
+    def name(self):
+        return self.group_name.split("/")[-1]
+
+    @property
+    def category(self):
+        return self.attrs["category"]
 
     def create_outlines(self, outlines: Iterable, sampling=None):
         outlines = self.create_dataset("outlines", data=outlines, dtype=np.float32)
@@ -69,6 +94,8 @@ class Entity(h5py.Group):
         orientations: Iterable = None,
         sampling: str = None,
     ):
+        poses, positions, orientations = utils.np_array(poses, positions, orientations)
+
         # Either poses or positions not both
         assert (
             poses is None or positions is None
@@ -80,8 +107,8 @@ class Entity(h5py.Group):
             )
         else:
             if poses is not None:
-                poses = np.array(poses)
-                assert poses.shape[1] == 3 or poses.shape[1] == 4
+
+                assert poses.shape[1] in [3, 4]
                 positions = poses[:, :2]
                 orientations = poses[:, 2:]
             if orientations is not None and orientations.shape[1] == 1:
@@ -90,20 +117,33 @@ class Entity(h5py.Group):
             positions = self.create_dataset(
                 "positions", data=positions, dtype=np.float32
             )
-            orientations = self.create_dataset(
-                "orientations", data=orientations, dtype=np.float32
-            )
+            if orientations is not None:
+                orientations = self.create_dataset(
+                    "orientations", data=orientations, dtype=np.float32
+                )
 
             if sampling is not None:
                 positions.attrs["sampling"] = sampling
                 orientations.attrs["sampling"] = sampling
 
-    def get_poses(self):
-        poses = np.concatenate([self["positions"], self["orientations"]], axis=1)
-        return poses
+    @property
+    def positions(self):
+        return self["positions"]
+
+    @property
+    def orientations(self):
+        if not "orientations" in self:
+            # If no orientation is given, the default direction is to the right
+            return np.tile([1, 0], (self.positions.shape[0], 1))
+        return self["orientations"]
+
+    @property
+    def poses(self):
+        return np.concatenate([self.positions, self.orientations], axis=1)
 
-    def get_poses_rad(self):
-        poses = self.get_poses()
+    @property
+    def poses_rad(self):
+        poses = self.poses
         # calculate the angles from the orientation vectors, write them to the third row and delete the fourth row
         poses[:, 2] = np.arctan2(poses[:, 3], poses[:, 2])
         poses = poses[:, :3]
diff --git a/src/robofish/io/file.py b/src/robofish/io/file.py
index deefcc9a771d6e54966224be4be1dc1abbb69c16..3b2f1fc7a3fefb825fb46fd79e94f3fc7dd44bb4 100644
--- a/src/robofish/io/file.py
+++ b/src/robofish/io/file.py
@@ -28,8 +28,6 @@ import tempfile
 import uuid
 import deprecation
 
-
-temp_dir = tempfile.TemporaryDirectory()
 default_format_version = np.array([1, 0], dtype=np.int32)
 
 default_format_url = (
@@ -38,16 +36,18 @@ default_format_url = (
 
 
 class File(h5py.File):
-    """ Represents a hdf5 file, which should be used to store data about the
-    movement and shape of individuals or swarms in time.
+    """ Represents a RoboFish Track Format file, which should be used to store tracking data of individual animals or swarms.
 
-    This class extends the h5py.File class, and behaves in the same way.
-    robofish io Files can be created, saved, loaded, and manipulated.
+    Files can be opened (with optional creation), modified inplace, and have copies of them saved.
     """
 
+    _temp_dir = None
+
     def __init__(
         self,
         path: Union[str, Path] = None,
+        mode: str = "r",
+        *,  # PEP 3102
         world_size_cm: [int, int] = None,
         strict_validate: bool = False,
         format_version: [int, int] = default_format_version,
@@ -57,25 +57,55 @@ class File(h5py.File):
         monotonic_time_points_us: Iterable = None,
         calendar_time_points: Iterable = None,
     ):
-        """ Constructor for the File class.
-
-        The constructor should either be called with a path, when loading an existing file,
-        or when a new file should be created with a world size.
-
-        Args:
-            path: optional path to a io file as a string or path object. The file will be loaded.
-            world_size_cm: optional integer array of the world size in cm
-            strict_validate: optional boolean, if the file should be strictly validated, when loaded from a path. The default is False.
-            format_version: optional version [major, minor] of the trackformat specification
+        """Create a new RoboFish Track Format object.
+
+        When called with a path, it is loaded, otherwise a new temporary file is created.
+
+        Parameters
+        ----------
+        path : str or Path, optional
+            Location of file to be opened. If not provided, mode is ignored.
+
+        mode : str
+            r        Readonly, file must exist (default)
+            r+       Read/write, file must exist
+            w        Create file, truncate if exists
+            x        Create file, fail if exists
+            a        Read/write if exists, create otherwise
+
+        world_size_cm
+            optional integer array of the world size in cm
+        strict_validate
+            optional boolean, if the file should be strictly validated, when loaded from a path. The default is False.
+        format_version
+            optional version [major, minor] of the trackformat specification
         """
 
-        self._name = str(uuid.uuid4())
-        self._tf_path = Path(temp_dir.name) / self._name
-        self.load(path, strict_validate)
-
-        if path is None or not Path(path).exists():
+        if path is None:
+            if type(self)._temp_dir is None:
+                type(self)._temp_dir = tempfile.TemporaryDirectory(
+                    prefix="robofish-io-"
+                )
+            super().__init__(
+                Path(type(self)._temp_dir.name) / str(uuid.uuid4()),
+                mode="x",
+                driver="core",
+                backing_store=True,
+                libver=("earliest", "v110"),
+            )
+            initialize = True
+        else:
+            # mode
+            # r        Readonly, file must exist (default)
+            # r+       Read/write, file must exist
+            # w        Create file, truncate if exists
+            # x        Create file, fail if exists
+            # a        Read/write if exists, create otherwise
+            logging.info(f"Opening File {path}")
+            initialize = not Path(path).exists()
+            super().__init__(path, mode, libver=("earliest", "v110"))
 
-            # Initialize new file
+        if initialize:
             assert world_size_cm is not None and format_version is not None
 
             self.attrs["world_size_cm"] = np.array(world_size_cm, dtype=np.float32)
@@ -93,58 +123,34 @@ class File(h5py.File):
                     calendar_time_points=calendar_time_points,
                     default=True,
                 )
+        self.validate(strict_validate)
 
-    #### File Handling ####
-    def load(self, path: Union[str, Path], strict_validate: bool = False) -> None:
-        """ Load a new file from a path
+    def __enter__(self):
+        return self
 
-        Args:
-            path: path to a io file as a string or path object
-            strict_validate: optional boolean, if the file should be strictly validated, when loaded from a path. The default is False.
-        """
-        if path is not None:
-            self._f_path = Path(path)
-
-        if path is not None and Path(path).exists():
-            logging.info(f"Opening File {path}")
-            shutil.copyfile(self._f_path, self._tf_path)
-            super().__init__(self._tf_path, "r+")
-            self.validate(strict_validate)
-        else:
-            super().__init__(self._tf_path, "w")
+    def __exit__(self, type, value, traceback):
+        # Check if the context was left under normal circumstances
+        if (type, value, traceback) == (None, None, None):
+            self.validate()
+            self.close()
 
-    def save(self, path: Union[str, Path] = None, strict_validate: bool = True):
-        """ Load a new file from a path
+    def save_as(self, path: Union[str, Path], strict_validate: bool = True):
+        """ Save a copy of the file
 
         Args:
-            path: optional path to a io file as a string or path object. If no path is specified, the last known path (from loading or saving) is used.
+            path: path to a io file as a string or path object. If no path is specified, the last known path (from loading or saving) is used.
             strict_validate: optional boolean, if the file should be strictly validated, before saving. The default is True.
         """
 
-        # Normaly only valid files can be saved
         self.validate(strict_validate=strict_validate)
 
-        # Find the correct path
-        if path is None:
-            if self._f_path is None:
-                raise Exception(
-                    "path was not specified and there was no saved path from loading or an earlier save"
-                )
-        else:
-            self._f_path = Path(path)
-
-        # Close the temporary file
-        self.close()
+        # Ensure all buffered data has been written to disk
+        self.flush()
 
-        # Create the parent folder if it does not exist
-        if not self._f_path.parent.exists():
-            self._f_path.parent.mkdir(parents=True, exist_ok=True)
+        path = Path(path).resolve()
+        path.parent.mkdir(parents=True, exist_ok=True)
 
-        # Copy the temporaryself.create_sampling(frequency_hz, monotonic_time_points_us) file to the path
-        shutil.copyfile(self._tf_path, self._f_path)
-
-        # Reopen the temporary file
-        super().__init__(self._tf_path, "r+")
+        shutil.copyfile(Path(self.filename).resolve(), path)
 
     def create_sampling(
         self,
@@ -202,11 +208,22 @@ class File(h5py.File):
             )
 
         if default:
-            self.default_sampling = name
-            self["samplings"].attrs["default"] = self.default_sampling
+            self["samplings"].attrs["default"] = name
         return name
 
-    def get_frequency(self):
+    @property
+    def world_size(self):
+        return self.attrs["world_size_cm"]
+
+    @property
+    def default_sampling(self):
+        if "default" in self["samplings"].attrs:
+            return self["samplings"].attrs["default"]
+        return None
+
+    @property
+    def frequency(self):
+        # NOTE: Only works if default sampling availabe and specified with frequency_hz.
         default_sampling = self["samplings"].attrs["default"]
         return self["samplings"][default_sampling].attrs["frequency_hz"]
 
@@ -233,11 +250,10 @@ class File(h5py.File):
             Name of the created entity
         """
 
-        if sampling is None:
-            if not hasattr(self, "default_sampling"):
-                raise Exception(
-                    "There was no sampling specified, when creating the file, nor when creating the entity."
-                )
+        if sampling is None and self.default_sampling is None:
+            raise Exception(
+                "There was no sampling specified, when creating the file, nor when creating the entity."
+            )
 
         entity = robofish.io.Entity.create_entity(
             self["entities"],
@@ -273,18 +289,18 @@ class File(h5py.File):
         """
 
         assert poses.ndim == 3
-        assert poses.shape[2] == 4
-        n = poses.shape[0]
+        assert poses.shape[2] in [3, 4]
+        agents = poses.shape[0]
         timesteps = poses.shape[1]
-        returned_names = []
+        entity_names = []
 
-        for i in range(n):
+        for i in range(agents):
             e_name = None if names is None else names[i]
             e_outline = (
                 outlines if outlines is None or outlines.ndim == 3 else outlines[i]
             )
 
-            returned_names.append(
+            entity_names.append(
                 self.create_entity(
                     category=category,
                     sampling=sampling,
@@ -293,15 +309,10 @@ class File(h5py.File):
                     outlines=e_outline,
                 )
             )
-        return returned_names
-
-    def get_entities(self):
-        return {
-            e_name: robofish.io.Entity.from_h5py_group(e_group)
-            for e_name, e_group in self["entities"].items()
-        }
+        return entity_names
 
-    def get_entity_names(self) -> Iterable[str]:
+    @property
+    def entity_names(self) -> Iterable[str]:
         """ Getter for the names of all entities
 
         Returns:
@@ -309,8 +320,24 @@ class File(h5py.File):
         """
         return sorted(self["entities"].keys())
 
-    def get_poses(self, names: Iterable = None, category: str = None) -> Iterable:
-        """ Get an array of the poses of entities
+    @property
+    def entities(self):
+        return [
+            robofish.io.Entity.from_h5py_group(self["entities"][name])
+            for name in self.entity_names
+        ]
+
+    @property
+    def entity_poses(self):
+        return self.select_entity_poses(None)
+
+    @property
+    def entity_poses_rad(self):
+        return self.select_entity_poses(None, rad=True)
+
+    def select_entity_poses(self, predicate=None, rad=False) -> Iterable:
+        """ TODO: Rework
+        Select an array of the poses of entities
 
         If no name or category is specified, all entities will be selected.
 
@@ -321,61 +348,51 @@ class File(h5py.File):
             An three dimensional array of all poses with the shape (entity, time, 4)
         """
 
-        if names is not None and category is not None:
-            logging.error("Specify either names or a category, not both.")
-            raise Exception
-
-        # collect the names of all entities with the correct category
-        if category is not None:
-            names = [
-                e_name
-                for e_name, e_data in self["entities"].items()
-                if e_data.attrs["category"] == category
-            ]
-
-        entities = self.get_entities()
-
-        # If no names or category are given, select all
-        if names is None:
-            names = sorted(entities.keys())
-
-        # Entity objects given as names
-        if all([type(name) == robofish.io.Entity for name in names]):
-            names = [entity.getName() for entity in names]
+        entities = self.entities
+        if predicate is not None:
+            entities = [e for e in entities if predicate(e)]
 
-        if not all([type(name) == str for name in names]):
-            raise Exception(
-                "Given names were not strings. Instead names were %s" % names
-            )
-
-        max_timesteps = (
-            0
-            if len(names) == 0
-            else max([entities[e_name]["positions"].shape[0] for e_name in names])
-        )
+        max_timesteps = max([0] + [e.positions.shape[0] for e in entities])
 
         # Initialize poses output array
-        poses_output = np.empty((len(names), max_timesteps, 4))
+        pose_len = 3 if rad else 4
+        poses_output = np.empty((len(entities), max_timesteps, pose_len))
         poses_output[:] = np.nan
 
         # Fill poses output array
         i = 0
         custom_sampling = None
-        for name in names:
-            entity = entities[name]
+        for entity in entities:
 
             if "sampling" in entity["positions"].attrs:
                 if custom_sampling is None:
                     custom_sampling = entity["positions"].attrs["sampling"]
                 elif custom_sampling != entity["positions"].attrs["sampling"]:
                     raise Exception(
-                        "Multiple samplings found, which can not be given back by the get_poses function collectively."
+                        "Multiple samplings found, preventing return of a single array."
                     )
-            poses = entity.get_poses()
+            poses = entity.poses_rad if rad else entity.poses
             poses_output[i][: poses.shape[0]] = poses
             i += 1
         return poses_output
 
+    @deprecation.deprecated(
+        deprecated_in="1.1.2",
+        removed_in="1.2",
+        details="get_poses() is deprecated and was replaced with the attribute 'poses' or the function select_poses(), when ",
+    )
+    def get_poses(self, *, category=None, names=None):
+
+        if category is not None:
+            predicate = lambda e: e.category == category
+            if names is not None:
+                predicate = lambda e: e.category == category and e.name in names
+        elif names is not None:
+            predicate = lambda e: e.name in names
+        else:
+            predicate = None
+        return self.select_entity_poses(predicate)
+
     def validate(self, strict_validate: bool = True) -> (bool, str):
         """Validate the file to the specification.
 
diff --git a/src/robofish/io/utils.py b/src/robofish/io/utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..71396419de0211166baf6ca2e7f752666ba14d84
--- /dev/null
+++ b/src/robofish/io/utils.py
@@ -0,0 +1,15 @@
+import robofish.io
+import numpy as np
+from pathlib import Path
+import os
+
+
+def np_array(*arrays):
+    result = tuple(np.array(a) if a is not None else None for a in arrays)
+    if len(result) == 1:
+        result = result[0]
+    return result
+
+
+def full_path(current_file, path):
+    return (Path(current_file).parent / path).resolve()
diff --git a/src/robofish/io/validation.py b/src/robofish/io/validation.py
index 776984b606fb89469a0d02cc860ac4489439ea63..a1bde527f16a2b28431c6b1148a4ecfec50d2354 100644
--- a/src/robofish/io/validation.py
+++ b/src/robofish/io/validation.py
@@ -150,7 +150,9 @@ def validate(iofile: File, strict_validate: bool = True) -> (bool, str):
 
         # validate entities
         assert_validate("entities" in iofile, "entities not found")
-        for e_name, entity in iofile.get_entities().items():
+        for entity in iofile.entities:
+            e_name = entity.name
+
             assert_validate(
                 type(entity) == Entity,
                 "Entity group was not a robofish.io.Entity object",
@@ -198,7 +200,9 @@ def validate(iofile: File, strict_validate: bool = True) -> (bool, str):
 
                 if positions.shape[0] > 0:
                     # validate range of poses
-                    validate_poses_range(iofile, positions, e_name)
+                    validate_positions_range(
+                        iofile.attrs["world_size_cm"], positions, e_name
+                    )
 
             if "orientations" in entity:
                 assert_validate(
@@ -226,6 +230,8 @@ def validate(iofile: File, strict_validate: bool = True) -> (bool, str):
                     e_name,
                 )
 
+                validate_orientations_length(orientations, e_name)
+
             # outlines
             if "outlines" in entity:
                 outlines = entity["outlines"]
@@ -289,37 +295,48 @@ def validate(iofile: File, strict_validate: bool = True) -> (bool, str):
     return (True, "")
 
 
-def validate_poses_range(iofile, poses, e_name):
-    # Poses which are just a bit over the world edge are fine
+def validate_positions_range(world_size, positions, e_name):
+    # positions which are just a bit over the world edge are fine
     error_allowance = 1.01
 
     allowed_x = [
-        -1 * iofile.attrs["world_size_cm"][0] * error_allowance / 2,
-        iofile.attrs["world_size_cm"][0] * error_allowance / 2,
+        -1 * world_size[0] * error_allowance / 2,
+        world_size[0] * error_allowance / 2,
     ]
 
-    real_x = [poses[:, 0].min(), poses[:, 0].max()]
+    real_x = [positions[:, 0].min(), positions[:, 0].max()]
 
     allowed_y = [
-        -1 * iofile.attrs["world_size_cm"][1] * error_allowance / 2.0,
-        iofile.attrs["world_size_cm"][1] * error_allowance / 2.0,
+        -1 * world_size[1] * error_allowance / 2.0,
+        world_size[1] * error_allowance / 2.0,
     ]
-    real_y = [poses[:, 1].min(), poses[:, 1].max()]
+    real_y = [positions[:, 1].min(), positions[:, 1].max()]
 
     assert_validate(
         allowed_x[0] <= real_x[0] and real_x[1] <= allowed_x[1],
-        "Poses of x axis were not in range. The allowed range is [%.1f, %.1f], which was [%.1f, %.1f] in the poses"
+        "Positions of x axis were not in range. The allowed range is [%.1f, %.1f], which was [%.1f, %.1f] in the Positions"
         % (allowed_x[0], allowed_x[1], real_x[0], real_x[1]),
         e_name,
     )
     assert_validate(
         allowed_y[0] <= real_y[0] and real_y[1] <= allowed_y[1],
-        "Poses of y axis were not in range. The allowed range is [%.1f, %.1f], which was [%.1f, %.1f] in the poses"
+        "Positions of y axis were not in range. The allowed range is [%.1f, %.1f], which was [%.1f, %.1f] in the Positions"
         % (allowed_y[0], allowed_y[1], real_y[0], real_y[1]),
         e_name,
     )
 
 
+def validate_orientations_length(orientations, e_name):
+    ori_lengths = np.linalg.norm(orientations, axis=1)
+
+    assert_validate(
+        np.isclose(ori_lengths, 1).all(),
+        "The orientation vectors were not unit vectors. Their length was in the range [%.2f, %.2f] when it should be 1"
+        % (min(ori_lengths), max(ori_lengths)),
+        e_name,
+    )
+
+
 def validate_iso8601(str_val: str) -> bool:
     """This function validates strings to match the ISO8601 format.
 
diff --git a/tests/resources/valid.hdf5 b/tests/resources/valid.hdf5
index 4da5ba546684a3f7abbd98172432639d096241f2..0c14ecdad78385832491ff147824f4e259701a05 100644
Binary files a/tests/resources/valid.hdf5 and b/tests/resources/valid.hdf5 differ
diff --git a/tests/robofish/evaluate/test_app_evaluate.py b/tests/robofish/evaluate/test_app_evaluate.py
new file mode 100644
index 0000000000000000000000000000000000000000..2a7fe486a95c961f41f591ea7c8ab533db6b2ccf
--- /dev/null
+++ b/tests/robofish/evaluate/test_app_evaluate.py
@@ -0,0 +1,28 @@
+import robofish.evaluate.app as app
+from robofish.io import utils
+import pytest
+import logging
+from pathlib import Path
+
+logging.getLogger().setLevel(logging.INFO)
+
+h5py_files = [utils.full_path(__file__, "../../resources/valid.hdf5")]
+graphics_out = utils.full_path(__file__, "output_graph.png")
+if graphics_out.exists():
+    graphics_out.unlink()
+
+
+def test_app_validate():
+    """ This tests the function of the robofish-io-validate command """
+
+    class DummyArgs:
+        def __init__(self, analysis_type):
+            self.analysis_type = analysis_type
+            self.paths = h5py_files
+            self.names = None
+            self.save_path = graphics_out
+
+    # TODO: Get rid of deprecation
+    with pytest.warns(DeprecationWarning):
+        app.evaluate(DummyArgs("speed"))
+    graphics_out.unlink()
diff --git a/tests/robofish/evaluate/test_evaluate.py b/tests/robofish/evaluate/test_evaluate.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/tests/robofish/io/test_app.py b/tests/robofish/io/test_app_io.py
similarity index 63%
rename from tests/robofish/io/test_app.py
rename to tests/robofish/io/test_app_io.py
index ac1dbe70223575f531f18d523e22986c919fc598..056d3d3191ea35d7a7ee5b62ac05e90b202c8bfd 100644
--- a/tests/robofish/io/test_app.py
+++ b/tests/robofish/io/test_app_io.py
@@ -1,4 +1,5 @@
 import robofish.io.app as app
+from robofish.io import utils
 import pytest
 import logging
 from pathlib import Path
@@ -6,11 +7,6 @@ from pathlib import Path
 logging.getLogger().setLevel(logging.INFO)
 
 
-#### Helpers ####
-def full_path(path):
-    return (Path(__file__).parent / path).resolve()
-
-
 def test_app_validate():
     """ This tests the function of the robofish-io-validate command """
 
@@ -19,11 +15,13 @@ def test_app_validate():
             self.path = path
             self.output_format = output_format
 
-    raw_output = app.validate(DummyArgs(full_path("../../resources"), "raw"))
+    raw_output = app.validate(
+        DummyArgs(utils.full_path(__file__, "../../resources"), "raw")
+    )
 
     # The three files valid.hdf5, almost_valid.hdf5, and invalid.hdf5 should be found.
     assert len(raw_output) == 2
-    app.validate(DummyArgs(full_path("../../resources"), "human"))
+    app.validate(DummyArgs(utils.full_path(__file__, "../../resources"), "human"))
 
 
 def test_app_print():
@@ -34,5 +32,9 @@ def test_app_print():
             self.path = path
             self.output_format = output_format
 
-    app.print(DummyArgs(full_path("../../resources/valid.hdf5"), "full"))
-    app.print(DummyArgs(full_path("../../resources/valid.hdf5"), "shape"))
+    app.print(
+        DummyArgs(utils.full_path(__file__, "../../resources/valid.hdf5"), "full")
+    )
+    app.print(
+        DummyArgs(utils.full_path(__file__, "../../resources/valid.hdf5"), "shape")
+    )
diff --git a/tests/robofish/io/test_entity.py b/tests/robofish/io/test_entity.py
index 0ebf09738a53d8f983b8a99f8c54de7c351b8bda..8c4ab6946a68a316bea62d2ac90a3be56176055a 100644
--- a/tests/robofish/io/test_entity.py
+++ b/tests/robofish/io/test_entity.py
@@ -3,16 +3,11 @@ import h5py
 import numpy as np
 
 
-#### Helpers ####
-def full_path(path):
-    return (Path(__file__).parent / path).resolve()
-
-
 def test_entity_object():
     sf = robofish.io.File(world_size_cm=[100, 100], frequency_hz=25)
     f = sf.create_entity("fish", positions=[[10, 10]])
     assert type(f) == robofish.io.Entity
-    assert f.getName() == "fish_1"
+    assert f.name == "fish_1"
     assert f.attrs["category"] == "fish"
     print(dir(f))
     print(f["positions"])
@@ -26,7 +21,7 @@ def test_entity_object():
     f2 = sf.create_entity("fish", poses=poses_rad)
     assert type(f2["positions"]) == h5py.Dataset
     assert type(f2["orientations"]) == h5py.Dataset
-    poses_rad_retrieved = f2.get_poses_rad()
+    poses_rad_retrieved = f2.poses_rad
 
     # Check if retrieved rad poses is close to the original poses.
     # Internally always ori_x and ori_y are used. When retrieved, the range is from -pi to pi, so for some of our original data 2 pi has to be substracted.
diff --git a/tests/robofish/io/test_examples.py b/tests/robofish/io/test_examples.py
new file mode 100644
index 0000000000000000000000000000000000000000..c81b1eed99221e9d92a82f0d354352fa869cfbae
--- /dev/null
+++ b/tests/robofish/io/test_examples.py
@@ -0,0 +1,45 @@
+import robofish.io
+from robofish.io import utils
+from pathlib import Path
+from testbook import testbook
+import sys
+
+
+sys.path.append(str(utils.full_path(__file__, "../../../examples/")))
+
+ipynb_path = utils.full_path(__file__, "../../../examples/example_basic.ipynb")
+path = utils.full_path(__file__, "../../../examples/tmp_example.hdf5")
+if path.exists():
+    path.unlink()
+
+
+def test_example_readme():
+    import example_readme
+
+    example_readme.create_example_file(path)
+    path.unlink()
+
+
+def test_example_basic():
+    import example_basic
+
+    example_basic.create_example_file(path)
+    path.unlink()
+
+
+# This test can be executed manually. The CI/CD System has issues with testbook.
+def manual_test_example_basic_ipynb():
+    # Executing the notebook should not lead to an exception
+    with testbook(str(ipynb_path), execute=True) as tb:
+        pass
+        # tb.ref("create_example_file")(path)
+    # path.unlink()
+
+
+if __name__ == "__main__":
+    print("example_readme.py")
+    test_example_readme()
+    print("example_basic.py")
+    test_example_basic()
+    print("example_basic.ipynb")
+    manual_test_example_basic_ipynb()
diff --git a/tests/robofish/io/test_file.py b/tests/robofish/io/test_file.py
index 3cb5d023681937c8d9334a6017de294820e2a128..31c54bc8382f82aa6376af26f08fcafe257f619a 100644
--- a/tests/robofish/io/test_file.py
+++ b/tests/robofish/io/test_file.py
@@ -1,4 +1,5 @@
 import robofish.io
+from robofish.io import utils
 import numpy as np
 from pathlib import Path
 import pytest
@@ -7,29 +8,30 @@ import datetime
 import sys
 import logging
 
+LOGGER = logging.getLogger(__name__)
 
-#### Helpers ####
-def full_path(path):
-    return (Path(__file__).parent / path).resolve()
-
-
-valid_file_path = full_path("../../resources/valid.hdf5")
-created_by_test_path = full_path("../../resources/created_by_test.hdf5")
-created_by_test_path_2 = full_path("../../resources/created_by_test_2.hdf5")
+valid_file_path = utils.full_path(__file__, "../../resources/valid.hdf5")
+created_by_test_path = utils.full_path(__file__, "../../resources/created_by_test.hdf5")
+created_by_test_path_2 = utils.full_path(
+    __file__, "../../resources/created_by_test_2.hdf5"
+)
 
 
 def test_constructor():
     sf = robofish.io.File(world_size_cm=[100, 100])
-    print(sf)
     sf.validate()
 
 
+def test_context():
+    with robofish.io.File(world_size_cm=[10, 10]) as f:
+        pass
+
+
 def test_new_file_w_path():
     sf = robofish.io.File(
-        created_by_test_path_2, world_size_cm=[100, 100], frequency_hz=25
+        created_by_test_path_2, "w", world_size_cm=[100, 100], frequency_hz=25
     )
     sf.create_entity("fish")
-    sf.save()
     sf.validate()
 
 
@@ -42,6 +44,7 @@ def test_missing_attribute():
 def test_single_entity_monotonic_step():
     sf = robofish.io.File(world_size_cm=[100, 100], frequency_hz=(1000 / 40))
     test_poses = np.ones(shape=(10, 4))
+    test_poses[:, 3] = 0  # All Fish pointing right
     sf.create_entity("robofish", poses=test_poses)
     sf.create_entity("object_without_poses")
     print(sf)
@@ -53,6 +56,7 @@ def test_single_entity_monotonic_time_points_us():
         world_size_cm=[100, 100], monotonic_time_points_us=np.ones(10)
     )
     test_poses = np.ones(shape=(10, 4))
+    test_poses[:, 3] = 0  # All Fish pointing right
     sf.create_entity("robofish", poses=test_poses)
     print(sf)
     sf.validate()
@@ -64,13 +68,13 @@ def test_multiple_entities():
 
     poses = np.zeros((agents, timesteps, 4))
     poses[1] = 1
-    poses[2] = 2
+    poses[:, :, 2:] = [1, 0]  # All agents point to the right
 
     m_points = np.arange(timesteps)
 
     sf = robofish.io.File(world_size_cm=[100, 100], monotonic_time_points_us=m_points)
     returned_entities = sf.create_multiple_entities("fish", poses)
-    returned_names = [entity.getName() for entity in returned_entities]
+    returned_names = [entity.name for entity in returned_entities]
 
     expected_names = ["fish_1", "fish_2", "fish_3"]
     print(returned_names)
@@ -80,24 +84,26 @@ def test_multiple_entities():
     sf.validate()
 
     # The returned poses should be equal to the inserted poses
-    returned_poses = sf.get_poses()
+    returned_poses = sf.entity_poses
     print(returned_poses)
     assert (returned_poses == poses).all()
 
     # Just get the array for some names
-    returned_poses = sf.get_poses(["fish_1", "fish_2"])
+    returned_poses = sf.select_entity_poses(lambda e: e.name in ["fish_1", "fish_2"])
     assert (returned_poses == poses[:2]).all()
 
-    # Falsely specify names and category
-    with pytest.raises(Exception):
-        sf.get_poses(names=["fish_1"], category="fish")
+    # Filter on both category and name
+    returned_poses = sf.select_entity_poses(
+        lambda e: e.category == "fish" and e.name == "fish_1"
+    )
+    assert (returned_poses == poses[:1]).all()
 
     # Insert some random obstacles
-    returned_names = sf.create_multiple_entities(
-        "obstacle", poses=np.random.random((agents, timesteps, 4))
-    )
+    obstacles = 3
+    obs_poses = np.random.random((agents, 1, 3))
+    returned_names = sf.create_multiple_entities("obstacle", poses=obs_poses)
     # Obstacles should not be returned when only fish are selected
-    returned_poses = sf.get_poses(category="fish")
+    returned_poses = sf.select_entity_poses(lambda e: e.category == "fish")
     assert (returned_poses == poses).all()
 
     # for each of the entities
@@ -113,29 +119,66 @@ def test_multiple_entities():
         monotonic_time_points_us=m_points, calendar_time_points=c_points
     )
 
-    try:
-        sf.create_sampling(frequency_hz=25, monotonic_time_points_us=m_points)
-        raise Exception("This sampling should have created an error")
-    except:
-        pass
-
     returned_names = sf.create_multiple_entities(
         "fish", poses, outlines=outlines, sampling=new_sampling
     )
     print(returned_names)
     print(sf)
 
-    # pass an poses array in separate parts (positions, orientations) and retreive it with get_poses.
+    # pass an poses array in separate parts (positions, orientations) and retrieve it with poses.
     poses_arr = np.random.random((100, 4))
+    poses_arr[:, 2:] /= np.atleast_2d(
+        np.linalg.norm(poses_arr[:, 2:], axis=1)
+    ).T  # Normalization
     position_orientation_fish = sf.create_entity(
         "fish", positions=poses_arr[:, :2], orientations=poses_arr[:, 2:]
     )
-    assert np.isclose(poses_arr, position_orientation_fish.get_poses()).all()
+    assert np.isclose(poses_arr, position_orientation_fish.poses).all()
     sf.validate()
 
     return sf
 
 
+def test_broken_sampling(caplog):
+    sf = robofish.io.File(world_size_cm=[10, 10])
+    caplog.set_level(logging.ERROR)
+    broken_sampling = sf.create_sampling(
+        name="broken sampling", frequency_hz=25, monotonic_time_points_us=np.ones((100))
+    )
+    assert "ERROR" in caplog.text
+
+
+def test_deprecated_get_poses():
+    f = test_multiple_entities()
+    with pytest.warns(DeprecationWarning):
+        assert f.get_poses().shape[0] == 10
+        assert f.get_poses(category="fish").shape[0] == 7
+        assert f.get_poses(names="fish_1").shape[0] == 1
+
+
+def test_entity_poses_rad(caplog):
+    with robofish.io.File(world_size_cm=[100, 100], frequency_hz=25) as f:
+        # Create an entity, using radiants
+        f.create_entity("fish", poses=np.ones((100, 3)))
+
+        # Read the poses of the file as radiants
+        np.testing.assert_almost_equal(f.entity_poses_rad, np.ones((1, 100, 3)))
+
+        caplog.set_level(logging.WARNING)
+        # Passing orientations, which are bigger than 2 pi
+        f.create_entity("fish", poses=np.ones((100, 3)) * 50)
+        assert "WARNING" in caplog.text
+
+
+def test_entity_positions_no_orientation():
+    with robofish.io.File(world_size_cm=[100, 100], frequency_hz=25) as f:
+        # Create an entity, using radiants
+        f.create_entity("fish", positions=np.ones((100, 2)))
+
+        assert f.entity_poses.shape == (1, 100, 4)
+        assert (f.entity_poses[:, :] == np.array([1, 1, 1, 0])).all()
+
+
 def test_load_validate():
     sf = robofish.io.File(path=valid_file_path)
     sf.validate()
@@ -143,10 +186,9 @@ def test_load_validate():
 
 def test_get_entity_names():
     sf = robofish.io.File(path=valid_file_path)
-    names = sf.get_entity_names()
-    assert len(names) == 9
+    names = sf.entity_names
+    assert len(names) == 1
     assert names[0] == "fish_1"
-    assert names[1] == "fish_2"
 
 
 def test_File_without_path_or_worldsize():
@@ -158,12 +200,18 @@ def test_loading_saving():
     sf = test_multiple_entities()
 
     assert not created_by_test_path.exists()
-    sf.save(created_by_test_path)
+    sf.save_as(created_by_test_path)
     assert created_by_test_path.exists()
 
     # After saving, the file should still be accessible and valid
     sf.validate()
 
+    # Open the file again and add another entity
+    sf = robofish.io.File(created_by_test_path, "r+")
+    entity = sf.create_entity("fish", positions=np.ones((100, 2)))
+    sf.entity_poses
+    entity.poses
+
 
 def test_validate_created_file_after_reloading():
     sf = robofish.io.File(created_by_test_path)
@@ -173,8 +221,9 @@ def test_validate_created_file_after_reloading():
 # Cleanup test. The z in the name makes sure, that it is executed last in main
 def test_z_cleanup():
     """ This cleans up after all tests and removes all test artifacts """
-    created_by_test_path.unlink()
-    created_by_test_path_2.unlink()
+    for f in [created_by_test_path, created_by_test_path_2]:
+        if f.exists():
+            f.unlink()
 
 
 if __name__ == "__main__":
diff --git a/tests/robofish/io/test_io.py b/tests/robofish/io/test_io.py
index bbd59dd70c49e70eaf697f4e0fe3d2c191d9ae29..f0609d2038a50d17a522479f2efbe57f3c13d301 100644
--- a/tests/robofish/io/test_io.py
+++ b/tests/robofish/io/test_io.py
@@ -1,11 +1,8 @@
 import robofish.io
+from robofish.io import utils
 import pytest
 from pathlib import Path
 
-#### Helpers ####
-def full_path(path):
-    return (Path(__file__).parent / path).resolve()
-
 
 def test_now_iso8061():
     # Example time: 2021-01-05T14:33:40.401+00:00
@@ -15,7 +12,7 @@ def test_now_iso8061():
 
 
 def test_read_multiple_single():
-    path = full_path("../../resources/valid.hdf5")
+    path = utils.full_path(__file__, "../../resources/valid.hdf5")
 
     # Variants path as posix path or as string
     for sf in [
@@ -29,7 +26,7 @@ def test_read_multiple_single():
 
 
 def test_read_multiple_folder():
-    path = full_path("../../resources/")
+    path = utils.full_path(__file__, "../../resources/")
 
     # Variants path as posix path or as string
     for sf in [