diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index d4b647ae4a8a493bfb4d579cbadab7ab87f2037e..082591d94a78d57029bb1aff67573ebcc2945af5 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -2,5 +2,6 @@ Unreleased changes
 ------------------
 
 * Add decay option to the runner script
+* Add muon propagation and upgrade tau propagation/decay being km3net geometry based
 
 
diff --git a/Dockerfile b/Dockerfile
index 625f65dfebc4915cc0456316ca224bd5db060493..383f160e28aedfd45eb460e812dd0f4e4501b567 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -28,6 +28,8 @@ RUN cd /km3buu && \
     pip3 install -e ".[dev]" && \
     pip3 install -e ".[extras]"
 
+RUN init4buu --proposal=/proposal
+
 RUN cd /km3buu/externals/km3net-dataformat/ && \
     make
 ENV KM3NET_LIB=/km3buu/externals/km3net-dataformat/lib    
diff --git a/km3buu/cmd.py b/km3buu/cmd.py
index 0e81b9d084a95ca6943f240b21ee9cff529d2b3b..f3019fc943d1385876e120450b0484981d27bef0 100755
--- a/km3buu/cmd.py
+++ b/km3buu/cmd.py
@@ -11,7 +11,7 @@ from os.path import join
 
 from km3buu.jobcard import generate_neutrino_jobcard
 from km3buu.ctrl import run_jobcard
-from km3buu.geometry import CanVolume, SphericalVolume, NoVolume
+from km3buu.geometry import *
 from km3buu.output import GiBUUOutput, write_detector_file
 
 ARGPARSE_DESC = {
@@ -71,7 +71,7 @@ ARGPARSE_GENERAL_PARAMS = [{
     "option_strings": ["--geometry", "-g"],
     "dest":
     "geometry",
-    "choices": ["no", "can", "sphere"],
+    "choices": ["no", "can", "sphere", "cylindrical"],
     "default":
     "no",
     "help":
@@ -97,7 +97,7 @@ ARGPARSE_GENERAL_PARAMS = [{
     "nargs":
     "*",
     "help":
-    "Dimensions of the geometry; sphere -> -d <radius> / can -> -d <radius> <zmin> <zmax>"
+    "Dimensions of the geometry; sphere -> -d <radius> / can -> -d <radius> <zmin> <zmax> / cylindrical -> -d <seawaterheight> <rockheight> <radius> <canradius> <canzmin> <canzmax>"
 }, {
     "option_strings": ["--output-dir", "-o"],
     "dest": "output",
@@ -245,7 +245,16 @@ def main():
             kwargs["zmin"] = args.dimensions[0]
             kwargs["zmax"] = args.dimensions[1]
             kwargs["radius"] = args.dimensions[2]
-        volume = CanVolume(**kwargs)
+        volume = CANVolume(**kwargs)
+    elif args.geometry == 'cylindrical':
+        kwargs = {"detector_center": tuple(args.center), "zenith": args.zenith}
+        kwargs["sw_height"] = args.dimensions[0]
+        kwargs["sr_height"] = args.dimensions[1]
+        kwargs["radius"] = args.dimensions[2]
+        kwargs["can_radius"] = args.dimensions[3]
+        kwargs["can_zmin"] = args.dimensions[4]
+        kwargs["can_zmax"] = args.dimensions[5]
+        volume = CylindricalVolume(**kwargs)
     run_descriptor = ""
     if args.runnumber:
         run_descriptor = "run{:08d}_".format(args.runnumber)
diff --git a/km3buu/geometry.py b/km3buu/geometry.py
index 27c685ec2d2bdbad50b47a787d644b43e2568d10..fbc1b86648342a72cef8ce866a72a04889cb2f07 100644
--- a/km3buu/geometry.py
+++ b/km3buu/geometry.py
@@ -13,6 +13,14 @@ __status__ = "Development"
 
 import numpy as np
 from abc import ABC, abstractmethod
+from collections import defaultdict
+from scipy.spatial.transform import Rotation
+from particle import Particle
+
+import proposal as pp
+from .propagation import *
+
+M_TO_CM = 1e2
 
 
 class DetectorVolume(ABC):
@@ -26,10 +34,15 @@ class DetectorVolume(ABC):
         self._solid_angle = 1
 
     @abstractmethod
-    def random_dir(self):
+    def random_dir(self, n=1):
         """
         Generate a random direction for the interaction
 
+        Parameter
+        ---------
+        n: int (default: 1)
+            Number of vertex positions to sample
+
         Return
         ------
         tuple [rad, 1] (phi, cos(theta))
@@ -37,17 +50,41 @@ class DetectorVolume(ABC):
         pass
 
     @abstractmethod
-    def random_pos(self):
+    def random_pos(self, n=1):
         """
         Generate a random position in the detector volume based on a uniform
         event distribution
 
+        Parameter
+        ---------
+        n: int (default: 1)
+            Number of vertex directions to sample
+
         Returns
         -------
         tuple [m] (x, y, z)
         """
         pass
 
+    def distribute_event(self, *pargs, **kwargs):
+        """
+        Integrated event distribution method which also handles propagation
+
+        Returns
+        -------
+        vtx_pos: tuple
+            Position of the vertex in the interaction volume
+        vtx_dir: tuple
+            Direction of the vertex in the interaction volume
+        weight_correction: float
+            If the sampling requires a correction to the weight this factor is
+            unequal 1
+        additional_events: awkward.highlevel.Array (
+            E, Px, Py, Pz, x, y, z, pdgid)
+            Propagated / Additional Particles from decays (None if no propagation is done)
+        """
+        return self.random_pos(), self.random_dir(), 1.0, None
+
     @abstractmethod
     def header_entries(self):
         """
@@ -77,7 +114,7 @@ class DetectorVolume(ABC):
 
         Returns
         -------
-        float [m^3] 
+        float [m^3]
         """
         return self._volume
 
@@ -95,7 +132,7 @@ class DetectorVolume(ABC):
 
 class NoVolume(DetectorVolume):
     """
-    Dummy volume to write out data w/o geometry 
+    Dummy volume to write out data w/o geometry
     """
 
     def __init__(self):
@@ -110,16 +147,29 @@ class NoVolume(DetectorVolume):
         retdct[key] = value
         return retdct
 
-    def random_pos(self):
-        return (.0, .0, .0)
+    def random_pos(self, n=1):
+        if n == 1:
+            return np.zeros(3)
+        else:
+            return np.zeros((n, 3))
+
+    def random_dir(self, n=1):
+        if n == 1:
+            return (0, 1)
+        else:
+            return np.concatenate([np.zeros(n), np.ones(n)]).reshape((2, -1)).T
 
-    def random_dir(self):
-        return (0, 1)
+    def distribute_event(self, evt):
+        vtx_pos = self.random_pos()
+        vtx_dir = self.random_dir()
+        weight = 1
+        evts = None
+        return vtx_pos, vtx_dir, weight, None
 
 
-class CanVolume(DetectorVolume):
+class CANVolume(DetectorVolume):
     """
-    Cylindrical detector geometry
+    Cylindrical detector geometry, only CAN / no propagation
 
     Parameters
     ----------
@@ -133,6 +183,8 @@ class CanVolume(DetectorVolume):
         Detector center position in the xy-plane
     zenith: float [1] (default: (-1.0, 1.0) )
         Zenith range given as cos(θ)
+    taupropagation: bool (default True)
+        Do secondary tau lepton propagation / decay
     """
 
     def __init__(self,
@@ -140,7 +192,8 @@ class CanVolume(DetectorVolume):
                  zmin=0.0,
                  zmax=476.5,
                  detector_center=(0., 0.),
-                 zenith=(-1, 1)):
+                 zenith=(-1, 1),
+                 taupropagation=True):
         super().__init__()
         self._radius = radius
         self._zmin = zmin
@@ -150,22 +203,80 @@ class CanVolume(DetectorVolume):
         self._cosZmin = zenith[0]
         self._cosZmax = zenith[1]
         self._solid_angle = 2 * np.pi * (self._cosZmax - self._cosZmin)
+        self._propagator = None
+        if taupropagation:
+            self._pp_geometry = self.make_proposal_geometries()
+            self._propagator = Propagator([15, -15], self._pp_geometry)
+
+    def make_proposal_geometries(self):
+        """
+        Setup the geometries for the propagation using PROPOSAL
+        """
+        geometries = dict()
+        # General
+        center_x = self._detector_center[0] * M_TO_CM
+        center_y = self._detector_center[1] * M_TO_CM
+        # StopVolume
+        geometry_stop = pp.geometry.Sphere(pp.Cartesian3D(), 1e20)
+        geometry_stop.hierarchy = PROPOSAL_STOP_HIERARCHY_LEVEL
+        geometries["stop"] = geometry_stop
+        # CAN
+        can_zpos = (self._zmin + self._zmax) / 2 * M_TO_CM
+        geometry_can = pp.geometry.Cylinder(
+            pp.Cartesian3D(center_x, center_y, can_zpos),
+            (self._zmax - self._zmin) * M_TO_CM, self._radius * M_TO_CM, 0.)
+        geometry_can.hierarchy = PROPOSAL_CAN_HIERARCHY_LEVEL
+        geometries["can"] = geometry_can
+        return geometries
 
     def _calc_volume(self):
         return np.pi * (self._zmax - self._zmin) * np.power(self._radius, 2)
 
-    def random_pos(self):
-        r = self._radius * np.sqrt(np.random.uniform(0, 1))
-        phi = np.random.uniform(0, 2 * np.pi)
+    def random_pos(self, n=1):
+        r = self._radius * np.sqrt(np.random.uniform(0, 1, n))
+        phi = np.random.uniform(0, 2 * np.pi, n)
         pos_x = r * np.cos(phi) + self._detector_center[0]
         pos_y = r * np.sin(phi) + self._detector_center[1]
-        pos_z = np.random.uniform(self._zmin, self._zmax)
-        return (pos_x, pos_y, pos_z)
+        pos_z = np.random.uniform(self._zmin, self._zmax, n)
+        pos = np.concatenate([pos_x, pos_y, pos_z]).reshape((3, -1)).T
+        if pos.shape[0] == 1:
+            return pos[0, :]
+        else:
+            return pos
+
+    def in_can(self, pos):
+        """
+        Check if position is inside the CAN
+
+        Parameters
+        ----------
+        pos: np.array
+            The positions which should be checked
 
-    def random_dir(self):
-        phi = np.random.uniform(0, 2 * np.pi)
-        cos_theta = np.random.uniform(self._cosZmin, self._cosZmax)
-        return (phi, cos_theta)
+        Return
+        ------
+        boolean / np.array
+        """
+        if type(pos) is tuple or pos.ndim == 1:
+            pos = np.reshape(pos, (-1, 3))
+        zmask = (pos[:, 2] >= self._zmin) & (pos[:, 2] <= self._zmax)
+        r2 = (pos[:, 0] - self._coord_origin[0])**2 + \
+            (pos[:, 1] - self._coord_origin[1])**2
+        rmask = r2 < (self._radius**2)
+        mask = zmask & rmask
+        if len(mask) == 1:
+            return mask[0]
+        else:
+            return mask
+
+    def random_dir(self, n=1):
+        phi = np.random.uniform(0, 2 * np.pi, n)
+        cos_theta = np.random.uniform(self._cosZmin, self._cosZmax, n)
+        direction = np.concatenate([phi, cos_theta]).reshape((2, -1)).T
+        if direction.shape[0] == 1:
+            return direction[0, :]
+        else:
+            return direction
 
     def header_entries(self, nevents=0):
         retdct = dict()
@@ -174,9 +285,245 @@ class CanVolume(DetectorVolume):
                                         self._volume, nevents)
         retdct[key] = value
         key = "fixedcan"
-        value = "0 0 {} {} {}".format(self._zmin, self._zmax, self._radius)
+        value = "{} {} {} {} {}".format(self._detector_center[0],
+                                        self._detector_center[1], self._zmin,
+                                        self._zmax, self._radius)
+        return retdct
+
+    def distribute_event(self, evt):
+        vtx_pos = self.random_pos()
+        vtx_angles = self.random_dir()
+        weight = 1
+        evts = None
+
+        if evt.flavor_ID == 3 and abs(evt.process_ID) == 2:
+            charged_lepton_type = np.sign(
+                evt.process_ID) * (2 * evt.flavor_ID + 9)
+            lepout_dir = np.array(
+                [evt.lepOut_Px, evt.lepOut_Py, evt.lepOut_Pz])
+            R = Rotation.from_euler("yz", vtx_angles)
+            evts = self._propagator.propagate(charged_lepton_type,
+                                              evt.lepOut_E, vtx_pos,
+                                              R.apply(lepout_dir))
+
+        return vtx_pos, vtx_angles, weight, evts
+
+
+class CylindricalVolume(DetectorVolume):
+    """
+    Cylindrical detector geometry
+
+    Parameters
+    ----------
+    radius: float [m] (default: 403.5)
+        Radius of the interaction volume
+    sw_height: float [m] (default: xxx)
+        Height of the seawater in the interaction volume
+    sr_height: float [m] (default: xxx)
+        Height of the seabottom rock in the interaction volume
+    can_radius: float [m] (default: 0.0)
+        Cylinder bottom z position
+    can_zmin: float [m] (default: 0.0)
+        Cylinder bottom z position
+    can_zmax: float [m] (default: 476.5)
+        Cylinder top z position
+    detector_center: tuple [m] (default: (0.0, 0.0) )
+        Detector center position in the xy-plane
+    zenith: float [1] (default: (-1.0, 1.0) )
+        Zenith range given as cos(θ)
+    """
+
+    def __init__(self,
+                 radius=500.0,
+                 sw_height=650.0,
+                 sr_height=100.0,
+                 can_radius=200.4,
+                 can_zmin=0.0,
+                 can_zmax=350,
+                 detector_center=(0., 0.),
+                 zenith=(-1, 1)):
+        super().__init__()
+        self._detector_center = detector_center
+        # Interaction Volume
+        self._radius = radius
+        self._water_height = sw_height
+        self._rock_height = sr_height
+        # Can Volume
+        self._canradius = can_radius
+        self._canzmin = can_zmin
+        self._canzmax = can_zmax
+        # Direction
+        self._cosZmin = zenith[0]
+        self._cosZmax = zenith[1]
+        # Properties
+        self._solid_angle = 2 * np.pi * (self._cosZmax - self._cosZmin)
+        self._volume = self._calc_volume()
+        # PROPOSAL
+        self._pp_geometry = None
+        self._propagator = None
+
+    def _calc_volume(self):
+        return np.pi * (self._water_height + self._rock_height) * np.power(
+            self._radius, 2)
+
+    def random_pos(self, n=1):
+        r = self._radius * np.sqrt(np.random.uniform(0, 1, n))
+        phi = np.random.uniform(0, 2 * np.pi, n)
+        pos_x = r * np.cos(phi) + self._detector_center[0]
+        pos_y = r * np.sin(phi) + self._detector_center[1]
+        pos_z = np.random.uniform(-self._rock_height, self._water_height, n)
+        pos = np.concatenate([pos_x, pos_y, pos_z]).reshape((3, -1)).T
+        if pos.shape[0] == 1:
+            return pos[0, :]
+        else:
+            return pos
+
+    def in_can(self, pos):
+        """
+        Check if position is inside the CAN
+
+        Parameters
+        ----------
+        pos: np.array
+            The positions which should be checked
+
+        Return
+        ------
+        boolean / np.array
+        """
+        if type(pos) is tuple or pos.ndim == 1:
+            pos = np.reshape(pos, (-1, 3))
+        zmask = (pos[:, 2] >= self._canzmin) & (pos[:, 2] <= self._canzmax)
+        r2 = (pos[:, 0] - self._coord_origin[0])**2 + \
+            (pos[:, 1] - self._coord_origin[1])**2
+        rmask = r2 < (self._canradius**2)
+        mask = zmask & rmask
+        if len(mask) == 1:
+            return mask[0]
+        else:
+            return mask
+
+    def random_dir(self, n=1):
+        phi = np.random.uniform(0, 2 * np.pi, n)
+        cos_theta = np.random.uniform(self._cosZmin, self._cosZmax, n)
+        direction = np.concatenate([phi, cos_theta]).reshape((2, -1)).T
+        if direction.shape[0] == 1:
+            return direction[0, :]
+        else:
+            return direction
+
+    def header_entries(self, nevents=0):
+        retdct = dict()
+        key = "genvol"
+        value = "{} {} {} {} {}".format(-self._rock_height, self._water_height,
+                                        self._radius, self._volume, nevents)
+        retdct[key] = value
+        key = "fixedcan"
+        value = "{} {} {} {} {}".format(self._detector_center[0],
+                                        self._detector_center[1],
+                                        self._canzmin, self._canzmax,
+                                        self._canradius)
         return retdct
 
+    def make_proposal_geometries(self):
+        """
+        Setup the geometries for the propagation using PROPOSAL
+        """
+        geometries = dict()
+        # General
+        center_x = self._detector_center[0] * M_TO_CM
+        center_y = self._detector_center[1] * M_TO_CM
+        # StopVolume
+        geometry_stop = pp.geometry.Sphere(pp.Cartesian3D(), 1e20)
+        geometry_stop.hierarchy = PROPOSAL_STOP_HIERARCHY_LEVEL
+        geometries["stop"] = geometry_stop
+        # CAN
+        can_zpos = (self._canzmin + self._canzmax) / 2 * M_TO_CM
+        geometry_can = pp.geometry.Cylinder(
+            pp.Cartesian3D(center_x, center_y, can_zpos),
+            (self._canzmax - self._canzmin) * M_TO_CM,
+            self._canradius * M_TO_CM, 0.)
+        geometry_can.hierarchy = PROPOSAL_CAN_HIERARCHY_LEVEL
+        geometries["can"] = geometry_can
+        # Interaction Volume
+        geometry_mantle = pp.geometry.Cylinder(
+            pp.Cartesian3D(center_x, center_y, can_zpos),
+            (self._canzmax - self._canzmin) * M_TO_CM, self._radius * M_TO_CM,
+            self._canradius * M_TO_CM)
+        geometry_mantle.hierarchy = PROPOSAL_PROPAGATION_HIERARCHY_LEVEL
+        geometries["can_mantle"] = geometry_mantle
+        if self._canzmin > 0:
+            floor_zpos = self._canzmin / 2 * M_TO_CM
+            geometry_floor = pp.geometry.Cylinder(
+                pp.Cartesian3D(center_x, center_y, floor_zpos),
+                (self._canzmax - self._canzmin) * M_TO_CM,
+                self._radius * M_TO_CM, 0.)
+            geometry_floor.hierarchy = PROPOSAL_PROPAGATION_HIERARCHY_LEVEL
+            geometries["can_floor"] = geometry_floor
+        if self._water_height > self._canzmax:
+            ceil_zpos = (self._water_height + self._canzmax) / 2 * M_TO_CM
+            geometry_ceil = pp.geometry.Cylinder(
+                pp.Cartesian3D(center_x, center_y, ceil_zpos),
+                (self._water_height - self._canzmax) * M_TO_CM,
+                self._radius * M_TO_CM, 0.)
+            geometry_ceil.hierarchy = PROPOSAL_PROPAGATION_HIERARCHY_LEVEL
+            geometries["can_ceiling"] = geometry_ceil
+        if self._rock_height > 0.:
+            sr_zpos = -self._rock_height / 2 * M_TO_CM
+            geometry_sr = pp.geometry.Cylinder(
+                pp.Cartesian3D(center_x, center_y, sr_zpos),
+                self._rock_height * M_TO_CM, self._radius * M_TO_CM, 0)
+            geometry_sr.hierarchy = PROPOSAL_PROPAGATION_HIERARCHY_LEVEL
+            geometries["sr"] = geometry_sr
+        return geometries
+
+    @staticmethod
+    def _addparticles(dct, particle_infos):
+        for prtcl in particle_infos:
+            dct['barcode'].append(prtcl.type)
+            dct['E'].append(prtcl.energy)
+            dct['x'].append(prtcl.position.x / 100)
+            dct['y'].append(prtcl.position.y / 100)
+            dct['z'].append(prtcl.position.z / 100)
+            dct['Px'].append(prtcl.direction.x * prtcl.momentum / 1e3)
+            dct['Py'].append(prtcl.direction.y * prtcl.momentum / 1e3)
+            dct['Pz'].append(prtcl.direction.z * prtcl.momentum / 1e3)
+            dct['deltaT'].append(prtcl.time)
+
+    def distribute_event(self, evt):
+        # NC -> doesn't require any propagation
+        if abs(evt.process_ID) == 3 or evt.flavor_ID == 1:
+            vtx_pos = self.random_pos()
+            vtx_dir = self.random_dir()
+            weight = 1
+            evts = None
+            return vtx_pos, vtx_dir, weight, evts
+
+        if not self._pp_geometry:
+            self._pp_geometry = self.make_proposal_geometries()
+
+        if not self._propagator:
+            self._propagator = Propagator([13, -13, 15, -15],
+                                          self._pp_geometry)
+
+        charged_lepton_type = np.sign(evt.process_ID) * (2 * evt.flavor_ID + 9)
+
+        samples = 0
+        lepout_dir = np.array([evt.lepOut_Px, evt.lepOut_Py, evt.lepOut_Pz])
+
+        while True:
+            samples += 1
+            vtx_pos = self.random_pos()
+            vtx_angles = self.random_dir()
+            if self.in_can(vtx_pos) and evt.flavor_ID == 2:
+                return vtx_pos, vtx_angles, samples, None
+            R = Rotation.from_euler("yz", vtx_angles)
+            particles = self._propagator.propagate(charged_lepton_type,
+                                                   evt.lepOut_E, vtx_pos,
+                                                   R.apply(lepout_dir))
+            if not particles is None:
+                return vtx_pos, vtx_angles, samples, particles
+
 
 class SphericalVolume(DetectorVolume):
     """
@@ -205,20 +552,29 @@ class SphericalVolume(DetectorVolume):
     def _calc_volume(self):
         return 4 / 3 * np.pi * np.power(self._radius, 3)
 
-    def random_pos(self):
-        r = self._radius * np.power(np.random.random(), 1 / 3)
-        phi = np.random.uniform(0, 2 * np.pi)
-        cosTheta = np.random.uniform(-1, 1)
-        pos_x = r * np.cos(phi) * np.sqrt(1 - np.power(cosTheta, 2))
-        pos_y = r * np.sin(phi) * np.sqrt(1 - np.power(cosTheta, 2))
-        pos_z = r * cosTheta
-        pos = (pos_x, pos_y, pos_z)
-        return tuple(np.add(self._coord_origin, pos))
-
-    def random_dir(self):
-        phi = np.random.uniform(0, 2 * np.pi)
-        cos_theta = np.random.uniform(self._cosZmin, self._cosZmax)
-        return (phi, cos_theta)
+    def random_pos(self, n=1):
+        r = self._radius * np.power(np.random.random(n), 1 / 3)
+        phi = np.random.uniform(0, 2 * np.pi, n)
+        cosTheta = np.random.uniform(-1, 1, n)
+        pos_x = r * np.cos(phi) * np.sqrt(
+            1 - np.power(cosTheta, 2)) + self._coord_origin[0]
+        pos_y = r * np.sin(phi) * np.sqrt(
+            1 - np.power(cosTheta, 2)) + self._coord_origin[1]
+        pos_z = r * cosTheta + self._coord_origin[2]
+        pos = np.concatenate([pos_x, pos_y, pos_z]).reshape((3, -1)).T
+        if pos.shape[0] == 1:
+            return pos[0, :]
+        else:
+            return pos
+
+    def random_dir(self, n=1):
+        phi = np.random.uniform(0, 2 * np.pi, n)
+        cos_theta = np.random.uniform(self._cosZmin, self._cosZmax, n)
+        direction = np.concatenate([phi, cos_theta]).reshape((2, -1)).T
+        if direction.shape[0] == 1:
+            return direction[0, :]
+        else:
+            return direction
 
     def header_entries(self, nevents=0):
         retdct = dict()
diff --git a/km3buu/output.py b/km3buu/output.py
index 357cc10014e5f0c8f31bbaae3adef5baea8aa30f..faa94ff4eb4de83cb37dccd58396acc8c23a5ec7 100644
--- a/km3buu/output.py
+++ b/km3buu/output.py
@@ -30,7 +30,7 @@ import km3io
 
 from .physics import visible_energy_fraction
 from .jobcard import Jobcard, read_jobcard, PDGID_LOOKUP
-from .geometry import DetectorVolume, CanVolume
+from .geometry import *
 from .config import Config, read_default_media_compositions
 from .__version__ import version
 
@@ -540,7 +540,6 @@ class GiBUUOutput:
                                self.flux_data["energy"][mask],
                                self.flux_data["flux"][mask],
                                p0=[1, -1])
-
         self._flux_index = popt[1]
 
     @property
@@ -552,7 +551,7 @@ def write_detector_file(gibuu_output,
                         ofile="gibuu.offline.root",
                         no_files=1,
                         run_number=1,
-                        geometry=CanVolume(),
+                        geometry=CylindricalVolume(),
                         livetime=3.156e7,
                         propagate_tau=True):  # pragma: no cover
     """
@@ -576,8 +575,8 @@ def write_detector_file(gibuu_output,
     if not isinstance(geometry, DetectorVolume):
         raise TypeError("Geometry needs to be a DetectorVolume")
 
-    def add_particles(particle_data, pos_offset, rotation, start_mc_trk_id,
-                      timestamp, status):
+    def add_particles(particle_data, start_mc_trk_id, timestamp, status,
+                      mother_id):
         nonlocal evt
         for i in range(len(particle_data.E)):
             trk = ROOT.Trk()
@@ -585,19 +584,13 @@ def write_detector_file(gibuu_output,
             mom = np.array([
                 particle_data.Px[i], particle_data.Py[i], particle_data.Pz[i]
             ])
-            p_dir = rotation.apply(mom / np.linalg.norm(mom))
-            try:
-                prtcl_pos = np.array([
-                    particle_data.x[i], particle_data.y[i], particle_data.z[i]
-                ])
-                prtcl_pos = rotation.apply(prtcl_pos)
-                trk.pos.set(*np.add(pos_offset, prtcl_pos))
-                trk.t = timestamp + particle_data.deltaT[i] * 1e9
-            except AttributeError:
-                trk.pos.set(*pos_offset)
-                trk.t = timestamp
+            p_dir = mom / np.linalg.norm(mom)
             trk.dir.set(*p_dir)
-            trk.mother_id = 0
+            prtcl_pos = np.array(
+                [particle_data.x[i], particle_data.y[i], particle_data.z[i]])
+            trk.pos.set(*prtcl_pos)
+            trk.t = timestamp + particle_data.deltaT[i]
+            trk.mother_id = mother_id
             trk.type = int(particle_data.barcode[i])
             trk.E = particle_data.E[i]
             trk.status = status
@@ -627,11 +620,6 @@ def write_detector_file(gibuu_output,
     bjorkenx = event_data.Bx
     bjorkeny = event_data.By
 
-    tau_secondaries = None
-    if propagate_tau and abs(nu_type) == 16 and ichan == 2:
-        from .propagation import propagate_lepton
-        tau_secondaries = propagate_lepton(event_data, np.sign(nu_type) * 15)
-
     media = read_default_media_compositions()
     density = media["SeaWater"]["density"]  # [g/cm^3]
     element = mendeleev.element(gibuu_output.Z)
@@ -663,6 +651,8 @@ def write_detector_file(gibuu_output,
     header_dct["primary"] = "{:d}".format(nu_type)
     header_dct["start_run"] = str(run_number)
 
+    event_times = np.random.uniform(0, livetime, len(event_data))
+
     for i in range(no_files):
         start_id = 0
         stop_id = len(event_data)
@@ -677,44 +667,65 @@ def write_detector_file(gibuu_output,
         outfile = ROOT.TFile.Open(tmp_filename, "RECREATE")
         tree = ROOT.TTree("E", "KM3NeT Evt Tree")
         tree.Branch("Evt", evt, 32000, 4)
-        mc_trk_id = 0
-
-        for mc_event_id, event in enumerate(event_data[start_id:stop_id]):
+        from tqdm import tqdm
+        for mc_event_id, event in tqdm(enumerate(
+                event_data[start_id:stop_id])):
+            mc_trk_id = 0
+            total_id = start_id + mc_event_id
             evt.clear()
             evt.id = mc_event_id
             evt.mc_run_id = mc_event_id
+            # Vertex Positioning & Propagation
+            vtx_pos, vtx_angles, samples, prop_particles = geometry.distribute_event(
+                event)
             # Weights
             evt.w.push_back(geometry.volume)  # w1 (can volume)
-            evt.w.push_back(w2[start_id + mc_event_id])  # w2
+            evt.w.push_back(w2[total_id] / samples)  # w2
             evt.w.push_back(-1.0)  # w3 (= w2*flux)
             # Event Information (w2list)
             evt.w2list.resize(W2LIST_LENGTH)
-            evt.w2list[km3io.definitions.w2list_km3buu["W2LIST_KM3BUU_PS"]] = global_generation_weight
-            evt.w2list[km3io.definitions.w2list_km3buu["W2LIST_KM3BUU_EG"]] = gibuu_output.flux_index
-            evt.w2list[km3io.definitions.w2list_km3buu["W2LIST_KM3BUU_XSEC_MEAN"]] = mean_xsec_func(
-                event.lepIn_E)
-            evt.w2list[km3io.definitions.w2list_km3buu["W2LIST_KM3BUU_XSEC"]] = event.xsec
-            evt.w2list[km3io.definitions.w2list_km3buu["W2LIST_KM3BUU_TARGETA"]] = gibuu_output.A
-            evt.w2list[km3io.definitions.w2list_km3buu["W2LIST_KM3BUU_TARGETZ"]] = gibuu_output.Z
-            evt.w2list[km3io.definitions.w2list_km3buu["W2LIST_KM3BUU_BX"]] = bjorkenx[mc_event_id]
-            evt.w2list[km3io.definitions.w2list_km3buu["W2LIST_KM3BUU_BY"]] = bjorkeny[mc_event_id]
-            evt.w2list[km3io.definitions.w2list_km3buu["W2LIST_KM3BUU_CC"]] = ichan
-            evt.w2list[km3io.definitions.w2list_km3buu["W2LIST_KM3BUU_ICHAN"]] = SCATTERING_TYPE_TO_GENIE[
-                event.evType]
-            evt.w2list[km3io.definitions.w2list_km3buu["W2LIST_KM3BUU_VERINCAN"]] = 1
-            evt.w2list[km3io.definitions.w2list_km3buu["W2LIST_KM3BUU_LEPINCAN"]] = 1
-            evt.w2list[km3io.definitions.w2list_km3buu["W2LIST_KM3BUU_GIBUU_WEIGHT"]] = event.weight
-            evt.w2list[km3io.definitions.w2list_km3buu["W2LIST_KM3BUU_GIBUU_SCAT_TYPE"]] = event.evType
+            evt.w2list[km3io.definitions.w2list_km3buu[
+                "W2LIST_KM3BUU_PS"]] = global_generation_weight
+            evt.w2list[km3io.definitions.w2list_km3buu[
+                "W2LIST_KM3BUU_EG"]] = gibuu_output.flux_index
+            evt.w2list[km3io.definitions.w2list_km3buu[
+                "W2LIST_KM3BUU_XSEC_MEAN"]] = mean_xsec_func(event.lepIn_E)
+            evt.w2list[km3io.definitions.
+                       w2list_km3buu["W2LIST_KM3BUU_XSEC"]] = event.xsec
+            evt.w2list[km3io.definitions.
+                       w2list_km3buu["W2LIST_KM3BUU_TARGETA"]] = gibuu_output.A
+            evt.w2list[km3io.definitions.
+                       w2list_km3buu["W2LIST_KM3BUU_TARGETZ"]] = gibuu_output.Z
+            evt.w2list[km3io.definitions.w2list_km3buu[
+                "W2LIST_KM3BUU_BX"]] = bjorkenx[mc_event_id]
+            evt.w2list[km3io.definitions.w2list_km3buu[
+                "W2LIST_KM3BUU_BY"]] = bjorkeny[mc_event_id]
+            evt.w2list[
+                km3io.definitions.w2list_km3buu["W2LIST_KM3BUU_CC"]] = ichan
+            evt.w2list[km3io.definitions.w2list_km3buu[
+                "W2LIST_KM3BUU_ICHAN"]] = SCATTERING_TYPE_TO_GENIE[
+                    event.evType]
+            evt.w2list[
+                km3io.definitions.w2list_km3buu["W2LIST_KM3BUU_VERINCAN"]] = 1
+            evt.w2list[
+                km3io.definitions.w2list_km3buu["W2LIST_KM3BUU_LEPINCAN"]] = 1
+            evt.w2list[km3io.definitions.w2list_km3buu[
+                "W2LIST_KM3BUU_GIBUU_WEIGHT"]] = event.weight
+            evt.w2list[km3io.definitions.w2list_km3buu[
+                "W2LIST_KM3BUU_GIBUU_SCAT_TYPE"]] = event.evType
             # TODO
-            evt.w2list[km3io.definitions.w2list_km3buu["W2LIST_KM3BUU_DXSEC"]] = np.nan
-            evt.w2list[km3io.definitions.w2list_km3buu["W2LIST_KM3BUU_COLUMN_DEPTH"]] = np.nan
-            evt.w2list[km3io.definitions.w2list_km3buu["W2LIST_KM3BUU_P_EARTH"]] = np.nan
-            evt.w2list[km3io.definitions.w2list_km3buu["W2LIST_KM3BUU_WATER_INT_LEN"]] = np.nan
-
-            # Vertex Position
-            vtx_pos = np.array(geometry.random_pos())
+            evt.w2list[km3io.definitions.
+                       w2list_km3buu["W2LIST_KM3BUU_DXSEC"]] = np.nan
+            evt.w2list[km3io.definitions.
+                       w2list_km3buu["W2LIST_KM3BUU_COLUMN_DEPTH"]] = np.nan
+            evt.w2list[km3io.definitions.
+                       w2list_km3buu["W2LIST_KM3BUU_P_EARTH"]] = np.nan
+            evt.w2list[km3io.definitions.
+                       w2list_km3buu["W2LIST_KM3BUU_WATER_INT_LEN"]] = np.nan
+
+            timestamp = event_times[total_id]
             # Direction
-            phi, cos_theta = geometry.random_dir()
+            phi, cos_theta = vtx_angles
             sin_theta = np.sqrt(1 - cos_theta**2)
 
             dir_x = np.cos(phi) * sin_theta
@@ -725,8 +736,6 @@ def write_detector_file(gibuu_output,
             theta = np.arccos(cos_theta)
             R = Rotation.from_euler("yz", [theta, phi])
 
-            timestamp = np.random.uniform(0, livetime)
-
             nu_in_trk = ROOT.Trk()
             nu_in_trk.id = mc_trk_id
             mc_trk_id += 1
@@ -736,7 +745,8 @@ def write_detector_file(gibuu_output,
             nu_in_trk.dir.set(*direction)
             nu_in_trk.E = event.lepIn_E
             nu_in_trk.t = timestamp
-            nu_in_trk.status = km3io.definitions.trkmembers["TRK_ST_PRIMARYNEUTRINO"]
+            nu_in_trk.status = km3io.definitions.trkmembers[
+                "TRK_ST_PRIMARYNEUTRINO"]
             evt.mc_trks.push_back(nu_in_trk)
 
             lep_out_trk = ROOT.Trk()
@@ -751,21 +761,44 @@ def write_detector_file(gibuu_output,
             lep_out_trk.E = event.lepOut_E
             lep_out_trk.t = timestamp
 
-            if tau_secondaries is not None:
-                lep_out_trk.status = km3io.definitions.trkmembers["TRK_ST_UNDEFINED"]
+            if prop_particles is not None:
+                lep_out_trk.status = km3io.definitions.trkmembers[
+                    "TRK_ST_PROPLEPTON"]
+                if geometry.in_can(vtx_pos):
+                    generator_particle_state = km3io.definitions.trkmembers[
+                        "TRK_ST_FINALSTATE"]
+                else:
+                    generator_particle_state = km3io.definitions.trkmembers[
+                        "TRK_ST_UNDEFINED"]
             else:
-                lep_out_trk.status = km3io.definitions.trkmembers["TRK_ST_FINALSTATE"]
+                lep_out_trk.status = km3io.definitions.trkmembers[
+                    "TRK_ST_FINALSTATE"]
+                generator_particle_state = km3io.definitions.trkmembers[
+                    "TRK_ST_FINALSTATE"]
 
             evt.mc_trks.push_back(lep_out_trk)
 
-            if tau_secondaries is not None:
-                event_tau_sec = tau_secondaries[mc_event_id]
-                add_particles(event_tau_sec, vtx_pos, R, mc_trk_id, timestamp,
-                              km3io.definitions.trkmembers["TRK_ST_FINALSTATE"])
-                mc_trk_id += len(event_tau_sec.E)
+            event.x = np.ones(len(event.E)) * vtx_pos[0]
+            event.y = np.ones(len(event.E)) * vtx_pos[1]
+            event.z = np.ones(len(event.E)) * vtx_pos[2]
+
+            directions = R.apply(np.array([event.Px, event.Py, event.Pz]).T)
+            event.Px = directions[:, 0]
+            event.Py = directions[:, 1]
+            event.Pz = directions[:, 2]
+
+            event.deltaT = np.zeros(len(event.E))
+
+            add_particles(event, mc_trk_id, timestamp,
+                          generator_particle_state, 0)
+            mc_trk_id += len(event.E)
+
+            if prop_particles is not None:
+                add_particles(
+                    prop_particles, mc_trk_id, timestamp,
+                    km3io.definitions.trkmembers["TRK_ST_FINALSTATE"], 1)
+                mc_trk_id += len(prop_particles.E)
 
-            add_particles(event, vtx_pos, R, mc_trk_id, timestamp,
-                          km3io.definitions.trkmembers["TRK_ST_FINALSTATE"])
             tree.Fill()
 
         for k, v in geometry.header_entries(mc_event_id + 1).items():
diff --git a/km3buu/propagation.py b/km3buu/propagation.py
index 61b59a9cab0137a0ec79af8a2dfc3f9a0aa669a3..5dbb16cb0a1047c22863c44ee5829700b9bcda6a 100644
--- a/km3buu/propagation.py
+++ b/km3buu/propagation.py
@@ -22,6 +22,12 @@ import pathlib
 
 from .config import Config
 
+M_TO_CM = 1e2
+
+itp_table_path = Config().proposal_itp_tables
+pathlib.Path(itp_table_path).mkdir(parents=True, exist_ok=True)
+pp.InterpolationSettings.tables_path = itp_table_path
+
 PROPOSAL_LEPTON_DEFINITIONS = {
     11: pp.particle.EMinusDef,
     -11: pp.particle.EPlusDef,
@@ -37,16 +43,21 @@ PROPOSAL_LEPTON_DEFINITIONS = {
     -16: pp.particle.NuTauBarDef
 }
 
-PROPOSAL_TARGET = pp.medium.AntaresWater()
+PROPOSAL_TARGET_WATER = pp.medium.AntaresWater()
+PROPOSAL_TARGET_ROCK = pp.medium.StandardRock()
+
+PROPOSAL_STOP_HIERARCHY_LEVEL = 0
+PROPOSAL_PROPAGATION_HIERARCHY_LEVEL = 4
+PROPOSAL_CAN_HIERARCHY_LEVEL = 2
 
+PROPOSAL_MUON_STOP_CONDITION = 3
+PROPOSAL_TAU_STOP_CONDITION = 1
 
-def _setup_propagator(max_distance, particle_definition):
-    itp_table_path = Config().proposal_itp_tables
-    pathlib.Path(itp_table_path).mkdir(parents=True, exist_ok=True)
-    pp.InterpolationSettings.tables_path = itp_table_path
+
+def _setup_utility(particle_definition, target):
     crosssection = pp.crosssection.make_std_crosssection(
         particle_def=particle_definition,
-        target=PROPOSAL_TARGET,
+        target=target,
         interpolate=True,
         cuts=pp.EnergyCutSettings(np.inf, 0.05, False))
     collection = pp.PropagationUtilityCollection()
@@ -54,73 +65,120 @@ def _setup_propagator(max_distance, particle_definition):
     collection.interaction = pp.make_interaction(crosssection, True)
     collection.decay = pp.make_decay(crosssection, particle_definition, True)
     collection.time = pp.make_time(crosssection, particle_definition, True)
+    return pp.PropagationUtility(collection=collection)
 
-    utility = pp.PropagationUtility(collection=collection)
 
-    geometry = pp.geometry.Sphere(pp.Cartesian3D(), max_distance)
-    density_distr = pp.density_distribution.density_homogeneous(
-        PROPOSAL_TARGET.mass_density)
+def setup_propagator(particle_definition, proposal_geometries):
+    utility_sw = _setup_utility(particle_definition, PROPOSAL_TARGET_WATER)
+    utility_sr = _setup_utility(
+        particle_definition,
+        PROPOSAL_TARGET_ROCK) if "sr" in proposal_geometries.keys() else None
 
-    propagator = pp.Propagator(particle_definition,
-                               [(geometry, utility, density_distr)])
+    density_sw = pp.density_distribution.density_homogeneous(
+        PROPOSAL_TARGET_WATER.mass_density)
+    density_sr = pp.density_distribution.density_homogeneous(
+        PROPOSAL_TARGET_ROCK.mass_density)
 
-    return propagator
+    sectors = [(geometry, utility_sw, density_sw)
+               for k, geometry in proposal_geometries.items() if k != "sr"]
+    if "sr" in proposal_geometries.keys():
+        sectors.append((proposal_geometries["sr"], utility_sr, density_sr))
 
+    propagator = pp.Propagator(particle_definition, sectors)
 
-def propagate_lepton(lepout_data, pdgid):
-    """
-    Lepton propagation based on PROPOSAL
-
-    Parameters
-    ----------
-    lepout_data: awkward.highlevel.Array
-        Lepton data in the GiBUU output shape containing the fields 
-        'lepOut_E, lepOut_Px, lepOut_Py, lepOut_Pz'
-    pdgid:
-        The pdgid of the propagated lepton
-
-    Returns
-    -------
-    awkward.highlevel.Array (E, Px, Py, Pz, x, y, z)
-    """
-    lepton_info = Particle.from_pdgid(pdgid)
-    prop_range = const.c * lepton_info.lifetime * 1e11 * np.max(
-        lepout_data.lepOut_E) / lepton_info.mass  #[cm]
-
-    lepton_def = PROPOSAL_LEPTON_DEFINITIONS[pdgid]()
+    return propagator
 
-    propagator = _setup_propagator(10 * prop_range, lepton_def)
 
-    propagated_data = defaultdict(list)
+class Propagator(object):
+
+    def __init__(self, pdgids, geometry):
+        self._geometry = geometry
+        self._pdgids = pdgids
+        self._pp_propagators = dict()
+        self._setup()
+
+    def _setup(self):
+        for p in self._pdgids:
+            self._pp_propagators[p] = setup_propagator(
+                PROPOSAL_LEPTON_DEFINITIONS[p](), self._geometry)
+
+    # @geometry.setter
+    # def geometry(self, geodct):
+    #     self._geometry = geodct
+    #     # Update propagators
+    #     self._setup()
+
+    @staticmethod
+    def _addparticles(dct, particle_infos):
+        for prtcl in particle_infos:
+            dct['barcode'].append(prtcl.type)
+            dct['E'].append(prtcl.energy / 1e3)
+            dct['x'].append(prtcl.position.x / M_TO_CM)
+            dct['y'].append(prtcl.position.y / M_TO_CM)
+            dct['z'].append(prtcl.position.z / M_TO_CM)
+            dct['Px'].append(prtcl.direction.x * prtcl.momentum / 1e3)
+            dct['Py'].append(prtcl.direction.y * prtcl.momentum / 1e3)
+            dct['Pz'].append(prtcl.direction.z * prtcl.momentum / 1e3)
+            dct['deltaT'].append(prtcl.time * 1e-9)
+
+    def propagate_particle(self, prtcl_state):
+        stop_condition = PROPOSAL_MUON_STOP_CONDITION if abs(
+            prtcl_state.type) == 13 else PROPOSAL_TAU_STOP_CONDITION
+        return self._pp_propagators[prtcl_state.type].propagate(
+            prtcl_state, hierarchy_condition=stop_condition)
+
+    def propagate(self, lep_pdgid, lep_E, lep_pos, lep_dir):
+        """
+        Lepton propagation to can based on PROPOSAL
+
+        Parameters
+        ----------
+        vtx_pos: np.array
+            Vertex positions of the given events
+        lep_E: np.array
+            Outgoing lepton energy
+        lep_dir: np.array 
+            Lepton direction positions of the given events
+        pdgid:
+            The pdgid of the propagated lepton
+
+        Returns
+        -------
+        awkward.highlevel.Array (barcode, deltaT, E, Px, Py, Pz, x, y, z)
+        """
+        if not lep_pdgid in self._pdgids:
+            return None
 
-    for entry in lepout_data:
         init_state = pp.particle.ParticleState()
-        init_state.energy = entry.lepOut_E * 1e3
-        init_state.position = pp.Cartesian3D(0, 0, 0)
-        init_state.direction = pp.Cartesian3D(entry.lepOut_Px, entry.lepOut_Py,
-                                              entry.lepOut_Pz)
+        init_state.type = lep_pdgid
+        init_state.energy = lep_E * 1e3
+        init_state.direction = pp.Cartesian3D(lep_dir)
         init_state.direction.normalize()
-        track = propagator.propagate(init_state, 5 * prop_range)
-        secondaries = track.decay_products()
-
-        E = [s.energy / 1e3 for s in secondaries]
-        pdgid = [p.type for p in secondaries]
-        Px = [p.direction.x * p.momentum / 1e3 for p in secondaries]
-        Py = [p.direction.y * p.momentum / 1e3 for p in secondaries]
-        Pz = [p.direction.z * p.momentum / 1e3 for p in secondaries]
-        x = [p.position.x / 100 for p in secondaries]
-        y = [p.position.y / 100 for p in secondaries]
-        z = [p.position.z / 100 for p in secondaries]
-        dt = [p.time for p in secondaries]
-
-        propagated_data["E"].append(E)
-        propagated_data["Px"].append(Px)
-        propagated_data["Py"].append(Py)
-        propagated_data["Pz"].append(Pz)
-        propagated_data["barcode"].append(pdgid)
-        propagated_data["x"].append(x)
-        propagated_data["y"].append(y)
-        propagated_data["z"].append(z)
-        propagated_data["deltaT"].append(dt)
-
-    return ak.Array(propagated_data)
+        init_state.position = pp.Cartesian3D(*(lep_pos * M_TO_CM))
+
+        particle_dct = defaultdict(list)
+
+        track = self.propagate_particle(init_state)
+        fsi = track.final_state()
+        decay = track.decay_products()
+
+        if self._geometry['can'].is_inside(fsi.position, fsi.direction):
+            if len(decay) > 0:
+                self._addparticles(particle_dct, decay)
+            else:
+                self._addparticles(particle_dct, [fsi])
+        else:
+            for d in decay:
+                if d.type in self._pp_propagators.keys():
+                    track = self.propagate_particle(d)
+                    fsi = track.final_state()
+                    decay = track.decay_products()
+                    if self._geometry['can'].is_inside(fsi.position,
+                                                       fsi.direction):
+                        if len(decay) > 0:
+                            self._addparticles(particle_dct, decay)
+                        else:
+                            self._addparticles(particle_dct, [fsi])
+        if len(particle_dct["E"]) == 0:
+            return None
+        return ak.Array(particle_dct)
diff --git a/km3buu/tests/test_geometry.py b/km3buu/tests/test_geometry.py
index f41e6b134c1611b89bdb246b88f8d53671be098d..cdca746803bfb068f1be730434ce42021345ecd1 100644
--- a/km3buu/tests/test_geometry.py
+++ b/km3buu/tests/test_geometry.py
@@ -46,14 +46,14 @@ class TestSphere(unittest.TestCase):
 
     def test_limited_zenith(self):
         np.random.seed(1234)
-        geometry = CanVolume(zenith=(-0.4, 0.5))
+        geometry = CANVolume(zenith=(-0.4, 0.5))
         self.assertAlmostEqual(geometry.solid_angle, 5.654866776461628)
         direction = geometry.random_dir()
         self.assertAlmostEqual(direction[1], 0.15989789393584863)
-        geometry = CanVolume(zenith=(0.1, 0.3))
+        geometry = CANVolume(zenith=(0.1, 0.3))
         direction = geometry.random_dir()
         self.assertAlmostEqual(direction[1], 0.25707171674275386)
-        geometry = CanVolume(zenith=(-0.3, -0.2))
+        geometry = CANVolume(zenith=(-0.3, -0.2))
         direction = geometry.random_dir()
         self.assertAlmostEqual(direction[1], -0.2727407394717358)
 
@@ -61,7 +61,7 @@ class TestSphere(unittest.TestCase):
 class TestCan(unittest.TestCase):
 
     def setUp(self):
-        self.detector_geometry = CanVolume()
+        self.detector_geometry = CANVolume()
 
     def test_volume(self):
         volume = self.detector_geometry.volume
@@ -76,7 +76,7 @@ class TestCan(unittest.TestCase):
 
     def test_position(self):
         np.random.seed(1234)
-        detector_geometry = CanVolume(detector_center=(100, 100))
+        detector_geometry = CANVolume(detector_center=(100, 100))
         pos = detector_geometry.random_pos()
         self.assertAlmostEqual(pos[0], -27.07940486491587)
         self.assertAlmostEqual(pos[1], -22.54421157149173)
@@ -84,13 +84,13 @@ class TestCan(unittest.TestCase):
 
     def test_limited_zenith(self):
         np.random.seed(1234)
-        geometry = CanVolume(zenith=(-0.4, 0.5))
+        geometry = CANVolume(zenith=(-0.4, 0.5))
         self.assertAlmostEqual(geometry.solid_angle, 5.654866776461628)
         direction = geometry.random_dir()
         self.assertAlmostEqual(direction[1], 0.15989789393584863)
-        geometry = CanVolume(zenith=(0.1, 0.3))
+        geometry = CANVolume(zenith=(0.1, 0.3))
         direction = geometry.random_dir()
         self.assertAlmostEqual(direction[1], 0.25707171674275386)
-        geometry = CanVolume(zenith=(-0.3, -0.2))
+        geometry = CANVolume(zenith=(-0.3, -0.2))
         direction = geometry.random_dir()
         self.assertAlmostEqual(direction[1], -0.2727407394717358)
diff --git a/km3buu/tests/test_propagation.py b/km3buu/tests/test_propagation.py
index cca16021d167577f83fb79103aee3f7f2eccb5fc..abfc6597731f6cdf0d467508e68ac847b76717af 100644
--- a/km3buu/tests/test_propagation.py
+++ b/km3buu/tests/test_propagation.py
@@ -21,59 +21,82 @@ from thepipe.logger import get_logger
 import proposal as pp
 
 import awkward as ak
-from km3buu.propagation import propagate_lepton
+
+from km3buu.geometry import *
+from km3buu.propagation import *
 
 pp.RandomGenerator.get().set_seed(1234)
+np.random.seed(1234)
 
 
 class TestTauPropagation(unittest.TestCase):
 
     def setUp(self):
-        data = ak.Array({
-            "lepOut_E": [
-                17.45830624434573, 3.1180989952362594, 21.270059768902005,
-                5.262659790136034, 23.52185741888274
-            ],
-            "lepOut_Px": [
-                -0.42224402086330426, -1.0232258668453014, -0.5801431899058521,
-                -0.9038349288874724, 0.9022573877437422
-            ],
-            "lepOut_Py": [
-                0.3644190693190108, -0.24542303987320932, 0.24499631087268617,
-                -1.1060562370375715, -3.982173292871768
-            ],
-            "lepOut_Pz": [
-                17.35867612031871, 2.336148261778657, 21.186342871282157,
-                4.743161507744377, 23.096499191566885
-            ]
-        })
-        self.sec = propagate_lepton(data, 15)
+        prop = Propagator([15, -15], CANVolume().make_proposal_geometries())
+        self.position = np.array([10.0, 10.0, 100.0])
+        self.direction = np.array([1., 1., 0])
+        self.energy = 10.0
+        self.sec = prop.propagate(15, self.energy, self.position,
+                                  self.direction)
+
+    def test_secondary_momenta(self):
+        np.testing.assert_array_almost_equal(np.array(self.sec.E),
+                                             [4.141, 5.761, 0.098],
+                                             decimal=3)
+        np.testing.assert_array_almost_equal(np.array(self.sec.Px),
+                                             [3.271, 3.696, -0.009],
+                                             decimal=3)
+        np.testing.assert_array_almost_equal(np.array(self.sec.Py),
+                                             [2.48, 4.382, 0.097],
+                                             decimal=3)
+        np.testing.assert_array_almost_equal(np.array(self.sec.Pz),
+                                             [-0.549, 0.564, -0.015],
+                                             decimal=3)
+
+    def test_secondary_types(self):
+        np.testing.assert_array_equal(np.array(self.sec.barcode),
+                                      [11, 16, -12])
+
+    def test_secondary_positions(self):
+        np.testing.assert_array_almost_equal(np.array(self.sec.x),
+                                             [10.0, 10.0, 10.0],
+                                             decimal=1)
+        np.testing.assert_array_almost_equal(np.array(self.sec.y),
+                                             [10.0, 10.0, 10.0],
+                                             decimal=1)
+        np.testing.assert_array_almost_equal(np.array(self.sec.z),
+                                             [100., 100., 100.],
+                                             decimal=1)
+
+
+class TestMuonPropagation(unittest.TestCase):
+
+    def setUp(self):
+        prop = Propagator([13, -13],
+                          CylindricalVolume().make_proposal_geometries())
+        self.position = np.array([200.0, 200.0, 100.0])
+        self.direction = np.array([-1., -1., 0])
+        self.energy = 100.0
+        self.sec = prop.propagate(13, self.energy, self.position,
+                                  self.direction)
 
     def test_secondary_momenta(self):
-        np.testing.assert_array_almost_equal(np.array(self.sec[0].E),
-                                             [2.182, 13.348, 1.928],
+        np.testing.assert_array_almost_equal(np.array(self.sec.E), [77.102],
                                              decimal=3)
-        np.testing.assert_array_almost_equal(np.array(self.sec[0].Px),
-                                             [0.295, -0.48, -0.237],
+        np.testing.assert_array_almost_equal(np.array(self.sec.Px), [-54.519],
                                              decimal=3)
-        np.testing.assert_array_almost_equal(np.array(self.sec[0].Py),
-                                             [-0.375, 0.784, -0.044],
+        np.testing.assert_array_almost_equal(np.array(self.sec.Py), [-54.519],
                                              decimal=3)
-        np.testing.assert_array_almost_equal(np.array(self.sec[0].Pz),
-                                             [2.129, 13.316, 1.913],
+        np.testing.assert_array_almost_equal(np.array(self.sec.Pz), [-0.],
                                              decimal=3)
 
     def test_secondary_types(self):
-        np.testing.assert_array_equal(np.array(self.sec[0].barcode),
-                                      [13, 16, -14])
+        np.testing.assert_array_equal(np.array(self.sec.barcode), [13])
 
     def test_secondary_positions(self):
-        np.testing.assert_array_almost_equal(np.array(self.sec[0].x),
-                                             [-1.4e-05, -1.4e-05, -1.4e-05],
+        np.testing.assert_array_almost_equal(np.array(self.sec.x), [141.7],
                                              decimal=1)
-        np.testing.assert_array_almost_equal(np.array(self.sec[0].y),
-                                             [1.2e-05, 1.2e-05, 1.2e-05],
+        np.testing.assert_array_almost_equal(np.array(self.sec.y), [141.7],
                                              decimal=1)
-        np.testing.assert_array_almost_equal(np.array(self.sec[0].z),
-                                             [0., 0., 0.],
+        np.testing.assert_array_almost_equal(np.array(self.sec.z), [100.],
                                              decimal=1)
diff --git a/km3buu/utils/initials.py b/km3buu/utils/initials.py
new file mode 100755
index 0000000000000000000000000000000000000000..09b84f448059cf1e4a608dc86825b6615a7912fe
--- /dev/null
+++ b/km3buu/utils/initials.py
@@ -0,0 +1,56 @@
+#!/usr/bin/env python
+# coding=utf-8
+# Filename: initials.py
+# Author: Johannes Schumann <jschumann@km3net.de>
+"""
+Initialisation script for different KM3BUU parts
+
+Usage:
+    initials.py (--proposal=PROPOSAL_PATH)
+    initials.py (-h | --help)
+
+    PROPOSAL    setup crosssection tables of proposal based on the standard settings used in KM3BUU
+Options:
+    -h --help                 Show this screen.
+    --proposal=PROPOSAL_PATH  Do PROPOSAL initialisations and write tables to PP_PATH
+
+"""
+import logging
+from pathlib import Path
+
+FORMAT = '%(asctime)s %(levelname)s %(filename)s -- %(message)s'
+logging.basicConfig(format=FORMAT)
+logging.getLogger().setLevel(logging.INFO)
+
+
+def main():
+    from docopt import docopt
+    args = docopt(__doc__)
+    if args['--proposal']:
+        from km3buu.config import Config
+        tablepath = Path(args['--proposal'])
+        tablepath.mkdir(parents=True, exist_ok=True)
+        Config().proposal_itp_tables = str(tablepath.absolute())
+        logging.info(f"Writing PROPOSAL tables to: {tablepath}")
+
+        from km3buu.propagation import _setup_utility, PROPOSAL_LEPTON_DEFINITIONS, PROPOSAL_TARGET_WATER, PROPOSAL_TARGET_ROCK
+        import proposal as pp
+
+        _setup_utility(PROPOSAL_LEPTON_DEFINITIONS[13](),
+                       PROPOSAL_TARGET_WATER)
+        _setup_utility(PROPOSAL_LEPTON_DEFINITIONS[15](),
+                       PROPOSAL_TARGET_WATER)
+        _setup_utility(PROPOSAL_LEPTON_DEFINITIONS[-13](),
+                       PROPOSAL_TARGET_WATER)
+        _setup_utility(PROPOSAL_LEPTON_DEFINITIONS[-15](),
+                       PROPOSAL_TARGET_WATER)
+        _setup_utility(PROPOSAL_LEPTON_DEFINITIONS[13](), PROPOSAL_TARGET_ROCK)
+        _setup_utility(PROPOSAL_LEPTON_DEFINITIONS[15](), PROPOSAL_TARGET_ROCK)
+        _setup_utility(PROPOSAL_LEPTON_DEFINITIONS[-13](),
+                       PROPOSAL_TARGET_ROCK)
+        _setup_utility(PROPOSAL_LEPTON_DEFINITIONS[-15](),
+                       PROPOSAL_TARGET_ROCK)
+
+
+if __name__ == '__main__':
+    main()
diff --git a/setup.cfg b/setup.cfg
index 04163cf1c9b07ac3e6f782b28727ae79301644b5..231a83de2144393e9be5a9f6ff6be7f6b17533c9 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -50,6 +50,7 @@ install_requires =
     pandas
     mendeleev
     proposal>=7.5.1
+    km3io
 python_requires = >=3.6
 include_package_data = True
 package_dir =
@@ -80,11 +81,11 @@ dev =
     setuptools_scm
     yapf>=0.25
     km3net-testdata>=0.2.11
-    km3io
 
 [options.entry_points]
 console_scripts =
     km3buu = km3buu.cmd:main
+    init4buu = km3buu.utils.initials:main
 
 [options.package_data]
 * = *.mplstyle, *.py.typed