implements simulation

        :param list(date) grid: list of Monte Carlo grid dates
        :param int num_of_paths: number of Monte Carlo paths
        :param hashable seed: seed used for rnds initialisation (additional adjustment in place)
        :param int or None num_of_workers: number of parallel workers (default: cpu_count()),
            if None no parallel processing is used
        :param bool profiling: signal whether to use profiling, True means used, else not
        :return object: final consumer state

        It returns a list of lists.
        The list contains per path a list produced by consumer at observation dates

def run(self, grid=None, num_of_paths=2000, seed=0, num_of_workers=CPU_COUNT, profiling=False):
        """
        implements simulation

        :param list(date) grid: list of Monte Carlo grid dates
        :param int num_of_paths: number of Monte Carlo paths
        :param hashable seed: seed used for rnds initialisation (additional adjustment in place)
        :param int or None num_of_workers: number of parallel workers (default: cpu_count()),
            if None no parallel processing is used
        :param bool profiling: signal whether to use profiling, True means used, else not
        :return object: final consumer state

        It returns a list of lists.
        The list contains per path a list produced by consumer at observation dates
        """
        self.grid = sorted(set(grid))
        self.num_of_paths = num_of_paths
        self.num_of_workers = num_of_workers
        self.seed = seed

        # pre processing
        self.producer.initialize(self.grid, self.num_of_paths, self.seed)
        self.consumer.initialize(self.grid, self.num_of_paths, self.seed)

        if num_of_workers:
            # processing
            workers = list()
            queue = Queue()
            path_per_worker = int(num_of_paths // num_of_workers)
            start_path, stop_path = 0, path_per_worker
            for i in range(num_of_workers):
                if i == num_of_workers - 1:
                    stop_path = num_of_paths  # ensure exact num of path as required
                name = 'worker-%d' % i
                if profiling:
                    # display profile with `snakeviz worker-0.prof`
                    # if not installed `pip install snakeviz`
                    workers.append(Process(target=self._run_parallel_process_with_profiling,
                                           name=name,
                                           args=(start_path, stop_path, queue, name + '.prof')))
                else:
                    workers.append(Process(target=self._run_parallel_process,
                                           name=name,
                                           args=(start_path, stop_path, queue)))
                start_path, stop_path = stop_path, stop_path + path_per_worker
            for worker in workers:
                worker.start()

            # post processing
            for _ in range(num_of_workers):
                self.consumer.get(queue.get())
            for worker in workers:
                worker.join()
        else:
            self._run_process(0, num_of_paths)

        self.consumer.finalize()
        return self.consumer.result